1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_ctl.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : ejabberd command line admin tool |
5 |
|
%%% Created : 11 Jan 2004 by Alexey Shchepin <alexey@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
%%% @headerfile "ejabberd_ctl.hrl" |
27 |
|
|
28 |
|
%%% @doc Management of mongooseimctl commands and frontend to ejabberd commands. |
29 |
|
%%% |
30 |
|
%%% An mongooseimctl command is an abstract function identified by a |
31 |
|
%%% name, with a defined number of calling arguments, that can be |
32 |
|
%%% defined in any Erlang module and executed using mongooseimctl |
33 |
|
%%% administration script. |
34 |
|
%%% |
35 |
|
%%% Note: strings cannot have blankspaces |
36 |
|
%%% |
37 |
|
%%% Does not support commands that have arguments with ctypes: list, tuple |
38 |
|
|
39 |
|
|
40 |
|
-module(ejabberd_ctl). |
41 |
|
-author('alexey@process-one.net'). |
42 |
|
|
43 |
|
-export([start/0, |
44 |
|
process/1]). |
45 |
|
|
46 |
|
-ignore_xref([process/1, start/0]). |
47 |
|
|
48 |
|
-include("ejabberd_ctl.hrl"). |
49 |
|
-include("mongoose_logger.hrl"). |
50 |
|
|
51 |
|
-type cmd() :: {CallString :: string(), Args :: [string()], Desc :: string()}. |
52 |
|
|
53 |
|
-define(ASCII_SPACE_CHARACTER, $\s). |
54 |
|
-define(PRINT(Format, Args), io:format(lists:flatten(Format), Args)). |
55 |
|
-define(TIME_HMS_FORMAT, "~B days ~2.10.0B:~2.10.0B:~2.10.0B"). |
56 |
|
-define(a2l(A), atom_to_list(A)). |
57 |
|
|
58 |
|
%%----------------------------- |
59 |
|
%% Module |
60 |
|
%%----------------------------- |
61 |
|
|
62 |
|
-spec start() -> none(). |
63 |
|
start() -> |
64 |
:-( |
case init:get_plain_arguments() of |
65 |
|
[SNode | Args] -> |
66 |
:-( |
SNode1 = case string:tokens(SNode, "@") of |
67 |
|
[_Node, _Server] -> |
68 |
:-( |
SNode; |
69 |
|
_ -> |
70 |
:-( |
case net_kernel:longnames() of |
71 |
|
true -> |
72 |
:-( |
SNode ++ "@" ++ inet_db:gethostname() ++ |
73 |
|
"." ++ inet_db:res_option(domain); |
74 |
|
false -> |
75 |
:-( |
SNode ++ "@" ++ inet_db:gethostname(); |
76 |
|
_ -> |
77 |
:-( |
SNode |
78 |
|
end |
79 |
|
end, |
80 |
:-( |
Node = list_to_atom(SNode1), |
81 |
:-( |
Status = case rpc:call(Node, ?MODULE, process, [Args]) of |
82 |
|
{badrpc, Reason} -> |
83 |
:-( |
?PRINT("Failed RPC connection to the node ~p: ~p~n", |
84 |
|
[Node, Reason]), |
85 |
|
%% TODO: show minimal start help |
86 |
:-( |
?STATUS_BADRPC; |
87 |
|
S -> |
88 |
:-( |
S |
89 |
|
end, |
90 |
:-( |
halt(Status); |
91 |
|
_ -> |
92 |
:-( |
print_usage(), |
93 |
:-( |
halt(?STATUS_USAGE) |
94 |
|
end. |
95 |
|
|
96 |
|
%%----------------------------- |
97 |
|
%% Process |
98 |
|
%%----------------------------- |
99 |
|
|
100 |
|
%% @doc The commands status, stop and restart are defined here to ensure |
101 |
|
%% they are usable even if ejabberd is completely stopped. |
102 |
|
-spec process(_) -> integer(). |
103 |
|
process(["status"]) -> |
104 |
2 |
{InternalStatus, ProvidedStatus} = init:get_status(), |
105 |
2 |
MongooseStatus = get_mongoose_status(), |
106 |
2 |
?PRINT("~s", [format_status([{node, node()}, {internal_status, InternalStatus}, |
107 |
|
{provided_status, ProvidedStatus}, |
108 |
|
{mongoose_status, MongooseStatus}, |
109 |
|
{os_pid, os:getpid()}, get_uptime(), |
110 |
|
{dist_proto, get_dist_proto()}, |
111 |
|
{logs, mongoose_logs:get_log_files()}])]), |
112 |
2 |
case MongooseStatus of |
113 |
:-( |
not_running -> ?STATUS_ERROR; |
114 |
2 |
{running, _, _Version} -> ?STATUS_SUCCESS |
115 |
|
end; |
116 |
|
process(["stop"]) -> |
117 |
|
%%ejabberd_cover:stop(), |
118 |
:-( |
init:stop(), |
119 |
:-( |
?STATUS_SUCCESS; |
120 |
|
process(["restart"]) -> |
121 |
:-( |
init:restart(), |
122 |
:-( |
?STATUS_SUCCESS; |
123 |
|
process(["join_cluster", Arg]) when is_list(Arg) -> |
124 |
5 |
join_cluster(Arg); |
125 |
|
process(["join_cluster" | _]) -> |
126 |
1 |
cluster_command_usage(); |
127 |
|
process(["leave_cluster"]) -> |
128 |
6 |
leave_cluster(); |
129 |
|
process(["remove_from_cluster", Arg]) when is_list(Arg) -> |
130 |
1 |
remove_from_cluster(Arg); |
131 |
|
process(["remove_from_cluster" | _]) -> |
132 |
1 |
cluster_command_usage(); |
133 |
|
process(["graphql", Arg]) when is_list(Arg) -> |
134 |
2 |
Doc = list_to_binary(Arg), |
135 |
2 |
Ep = mongoose_graphql:get_endpoint(admin), |
136 |
2 |
Result = mongoose_graphql:execute_cli(Ep, undefined, Doc), |
137 |
2 |
handle_graphql_result(Result); |
138 |
|
process(["graphql" | _]) -> |
139 |
2 |
?PRINT("This command requires one string type argument!\n", []), |
140 |
2 |
?STATUS_ERROR; |
141 |
|
process(Args) -> |
142 |
638 |
case mongoose_graphql_commands:process(Args) of |
143 |
|
#{status := executed, result := Result} -> |
144 |
624 |
handle_graphql_result(Result); |
145 |
|
#{status := error, reason := no_args} = Ctx -> |
146 |
1 |
print_usage(Ctx), |
147 |
1 |
?STATUS_USAGE; |
148 |
|
#{status := error} = Ctx -> |
149 |
11 |
?PRINT(error_message(Ctx) ++ "\n\n", []), |
150 |
11 |
print_usage(Ctx), |
151 |
11 |
?STATUS_ERROR; |
152 |
|
#{status := usage} = Ctx -> |
153 |
2 |
print_usage(Ctx), |
154 |
2 |
?STATUS_SUCCESS % not STATUS_USAGE, as that would tell the script to print general help |
155 |
|
end. |
156 |
|
|
157 |
|
-spec error_message(mongoose_graphql_commands:context()) -> iolist(). |
158 |
|
error_message(#{reason := unknown_command, command := Command}) -> |
159 |
2 |
io_lib:format("Unknown command '~s'", [Command]); |
160 |
|
error_message(#{reason := invalid_args}) -> |
161 |
2 |
"Could not parse the command arguments"; |
162 |
|
error_message(#{reason := unknown_category}) -> |
163 |
2 |
"Unknown category"; |
164 |
|
error_message(#{reason := {unknown_arg, ArgName}, command := Command}) -> |
165 |
1 |
io_lib:format("Unknown argument '~s' for command '~s'", [ArgName, Command]); |
166 |
|
error_message(#{reason := {invalid_arg_value, ArgName, ArgValue}, command := Command}) -> |
167 |
1 |
io_lib:format("Invalid value '~s' of argument '~s' for command '~s'", |
168 |
|
[ArgValue, ArgName, Command]); |
169 |
|
error_message(#{reason := {missing_args, MissingArgs}, command := Command}) -> |
170 |
3 |
io_lib:format("Missing mandatory arguments for command '~s': ~s", |
171 |
|
[Command, ["'", lists:join("', '", MissingArgs), "'"]]). |
172 |
|
|
173 |
|
-spec print_usage(mongoose_graphql_commands:context()) -> any(). |
174 |
|
print_usage(#{category := Category, command := Command, args_spec := ArgsSpec}) -> |
175 |
8 |
print_usage_command(Category, Command, ArgsSpec); |
176 |
|
print_usage(#{category := Category, commands := Commands}) -> |
177 |
3 |
print_usage_category(Category, Commands); |
178 |
|
print_usage(_) -> |
179 |
3 |
{MaxC, ShCode} = get_shell_info(), |
180 |
3 |
print_usage(MaxC, ShCode). |
181 |
|
|
182 |
|
handle_graphql_result({ok, Result}) -> |
183 |
582 |
JSONResult = mongoose_graphql_response:term_to_pretty_json(Result), |
184 |
582 |
?PRINT("~s\n", [JSONResult]), |
185 |
582 |
case Result of |
186 |
223 |
#{errors := _} -> ?STATUS_ERROR; |
187 |
359 |
_ -> ?STATUS_SUCCESS |
188 |
|
end; |
189 |
|
handle_graphql_result({error, Reason}) -> |
190 |
44 |
{_Code, Error} = mongoose_graphql_errors:format_error(Reason), |
191 |
44 |
JSONResult = jiffy:encode(#{errors => [Error]}, [pretty]), |
192 |
44 |
?PRINT("~s\n", [JSONResult]), |
193 |
44 |
?STATUS_ERROR. |
194 |
|
|
195 |
|
%%----------------------------- |
196 |
|
%% Format arguments |
197 |
|
%%----------------------------- |
198 |
|
format_status([{node, Node}, {internal_status, IS}, {provided_status, PS}, |
199 |
|
{mongoose_status, MS}, {os_pid, OSPid}, {uptime, UptimeHMS}, |
200 |
|
{dist_proto, DistProto}, {logs, LogFiles}]) -> |
201 |
|
( ["MongooseIM node ", ?a2l(Node), ":\n", |
202 |
|
" operating system pid: ", OSPid, "\n", |
203 |
|
" Erlang VM status: ", ?a2l(IS), " (of: starting | started | stopping)\n", |
204 |
|
" boot script status: ", io_lib:format("~p", [PS]), "\n", |
205 |
|
" version: ", case MS of |
206 |
2 |
{running, App, Version} -> [Version, " (as ", ?a2l(App), ")"]; |
207 |
:-( |
not_running -> "unavailable - neither ejabberd nor mongooseim is running" |
208 |
|
end, "\n", |
209 |
|
" uptime: ", io_lib:format(?TIME_HMS_FORMAT, UptimeHMS), "\n", |
210 |
2 |
" distribution protocol: ", DistProto, "\n"] ++ |
211 |
:-( |
[" logs: none - maybe enable logging to a file in app.config?\n" || LogFiles == [] ] ++ |
212 |
2 |
[" logs:\n" || LogFiles /= [] ] ++ [ |
213 |
4 |
[" ", LogFile, "\n"] || LogFile <- LogFiles ] ). |
214 |
|
|
215 |
|
%%----------------------------- |
216 |
|
%% Print help |
217 |
|
%%----------------------------- |
218 |
|
|
219 |
|
%% Bold |
220 |
|
-define(B1, "\e[1m"). |
221 |
|
-define(B2, "\e[22m"). |
222 |
|
-define(B(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end). |
223 |
|
|
224 |
|
%% Underline |
225 |
|
-define(U1, "\e[4m"). |
226 |
|
-define(U2, "\e[24m"). |
227 |
|
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). |
228 |
|
|
229 |
|
print_usage() -> |
230 |
:-( |
{MaxC, ShCode} = get_shell_info(), |
231 |
:-( |
print_usage(MaxC, ShCode). |
232 |
|
|
233 |
|
|
234 |
|
-spec print_usage(MaxC :: integer(), ShCode :: boolean()) -> ok. |
235 |
|
print_usage(MaxC, ShCode) -> |
236 |
3 |
?PRINT(["Usage: ", ?B("mongooseimctl"), " [", ?U("category"), "] ", ?U("command"), |
237 |
3 |
" [", ?U("arguments"), "]\n\n" |
238 |
|
"Most MongooseIM commands are grouped into the following categories:\n"], []), |
239 |
3 |
print_categories(MaxC, ShCode), |
240 |
3 |
?PRINT(["\nTo list the commands in a particular category:\n mongooseimctl ", ?U("category"), |
241 |
|
"\n"], []), |
242 |
3 |
?PRINT(["\nThe following basic system management commands do not have a category:\n"], []), |
243 |
3 |
print_usage_commands(MaxC, ShCode, basic_commands()). |
244 |
|
|
245 |
|
-spec basic_commands() -> [cmd()]. |
246 |
|
basic_commands() -> |
247 |
3 |
[{"status", [], "Get MongooseIM status"}, |
248 |
|
{"stop", [], "Stop MongooseIM"}, |
249 |
|
{"restart", [], "Restart MongooseIM"}, |
250 |
|
{"graphql", ["query"], "Execute GraphQL query or mutation"}]. |
251 |
|
|
252 |
|
-spec print_categories(MaxC :: integer(), ShCode :: boolean()) -> ok. |
253 |
|
print_categories(MaxC, ShCode) -> |
254 |
3 |
SortedSpecs = lists:sort(maps:to_list(mongoose_graphql_commands:get_specs())), |
255 |
3 |
Categories = [{binary_to_list(Category), [], binary_to_list(Desc)} |
256 |
3 |
|| {Category, #{desc := Desc}} <- SortedSpecs], |
257 |
3 |
print_usage_commands(MaxC, ShCode, Categories). |
258 |
|
|
259 |
|
-spec print_usage_category(mongoose_graphql_commands:category(), |
260 |
|
mongoose_graphql_commands:command_map()) -> ok. |
261 |
|
print_usage_category(Category, Commands) -> |
262 |
3 |
{MaxC, ShCode} = get_shell_info(), |
263 |
3 |
?PRINT(["Usage: ", ?B("mongooseimctl"), " ", Category, " ", ?U("command"), " ", ?U("arguments"), "\n" |
264 |
|
"\n" |
265 |
|
"The following commands are available in the category '", Category, "':\n"], []), |
266 |
3 |
CmdSpec = [{binary_to_list(Command), [], binary_to_list(Desc)} |
267 |
3 |
|| {Command, #{desc := Desc}} <- maps:to_list(Commands)], |
268 |
3 |
print_usage_commands(MaxC, ShCode, CmdSpec), |
269 |
3 |
?PRINT(["\nTo list the arguments for a particular command:\n" |
270 |
3 |
" mongooseimctl ", Category, " ", ?U("command"), " --help", "\n"], []). |
271 |
|
|
272 |
|
-spec print_usage_command(mongoose_graphql_commands:category(), |
273 |
|
mongoose_graphql_commands:command(), |
274 |
|
[mongoose_graphql_commands:arg_spec()]) -> ok. |
275 |
|
print_usage_command(Category, Command, ArgsSpec) -> |
276 |
8 |
{MaxC, ShCode} = get_shell_info(), |
277 |
8 |
?PRINT(["Usage: ", ?B("mongooseimctl"), " ", Category, " ", Command, " ", ?U("arguments"), "\n" |
278 |
|
"\n", |
279 |
8 |
"Each argument has the format: --", ?U("name"), " ", ?U("value"), "\n", |
280 |
|
"Available arguments are listed below with the corresponding GraphQL types:\n"], []), |
281 |
|
%% Reuse the function initially designed for printing commands for now |
282 |
|
%% This will be replaced with new logic when old commands are dropped |
283 |
8 |
Args = [{binary_to_list(Name), [], mongoose_graphql_commands:wrap_type(Wrap, Type)} |
284 |
8 |
|| #{name := Name, type := Type, wrap := Wrap} <- ArgsSpec], |
285 |
8 |
print_usage_commands(MaxC, ShCode, Args), |
286 |
8 |
?PRINT(["\nScalar values do not need quoting unless they contain special characters or spaces.\n" |
287 |
|
"Complex input types are passed as JSON maps or lists, depending on the type.\n" |
288 |
|
"When a type is followed by '!', the corresponding argument is required.\n"], []). |
289 |
|
|
290 |
|
-spec print_usage_commands(MaxC :: integer(), |
291 |
|
ShCode :: boolean(), |
292 |
|
Commands :: [cmd(), ...]) -> 'ok'. |
293 |
|
print_usage_commands(MaxC, ShCode, Commands) -> |
294 |
17 |
CmdDescsSorted = lists:keysort(1, Commands), |
295 |
|
|
296 |
|
%% What is the length of the largest command? |
297 |
17 |
{CmdArgsLenDescsSorted, Lens} = |
298 |
|
lists:mapfoldl( |
299 |
|
fun({Cmd, Args, Desc}, Lengths) -> |
300 |
112 |
Len = |
301 |
|
length(Cmd) + |
302 |
|
lists:foldl(fun(Arg, R) -> |
303 |
3 |
R + 1 + length(Arg) |
304 |
|
end, |
305 |
|
0, |
306 |
|
Args), |
307 |
112 |
{{Cmd, Args, Len, Desc}, [Len | Lengths]} |
308 |
|
end, |
309 |
|
[], |
310 |
|
CmdDescsSorted), |
311 |
17 |
MaxCmdLen = case Lens of |
312 |
:-( |
[] -> 80; |
313 |
17 |
_ -> lists:max(Lens) |
314 |
|
end, |
315 |
|
|
316 |
|
%% For each command in the list of commands |
317 |
|
%% Convert its definition to a line |
318 |
17 |
FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode), |
319 |
17 |
?PRINT([FmtCmdDescs], []). |
320 |
|
|
321 |
|
%% @doc Get some info about the shell: how many columns of width and guess if |
322 |
|
%% it supports text formatting codes. |
323 |
|
-spec get_shell_info() -> {integer(), boolean()}. |
324 |
|
get_shell_info() -> |
325 |
14 |
case io:columns() of |
326 |
:-( |
{ok, C} -> {C-2, true}; |
327 |
14 |
{error, enotsup} -> {78, false} |
328 |
|
end. |
329 |
|
|
330 |
|
%% @doc Split this command description in several lines of proper length |
331 |
|
-spec prepare_description(DescInit :: non_neg_integer(), |
332 |
|
MaxC :: integer(), |
333 |
|
Desc :: string()) -> [[[any()]], ...]. |
334 |
|
prepare_description(DescInit, MaxC, Desc) -> |
335 |
112 |
Words = string:tokens(Desc, " \n"), |
336 |
112 |
prepare_long_line(DescInit, MaxC, Words). |
337 |
|
|
338 |
|
-spec prepare_long_line(DescInit :: non_neg_integer(), |
339 |
|
MaxC :: integer(), |
340 |
|
Words :: [nonempty_string()] |
341 |
|
) -> [[[any()]], ...]. |
342 |
|
prepare_long_line(DescInit, MaxC, Words) -> |
343 |
112 |
MaxSegmentLen = MaxC - DescInit, |
344 |
112 |
MarginString = lists:duplicate(DescInit, ?ASCII_SPACE_CHARACTER), % Put spaces |
345 |
112 |
[FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words), |
346 |
112 |
MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments), |
347 |
112 |
[FirstSegment | MoreSegmentsMixed]. |
348 |
|
|
349 |
|
-spec mix_desc_segments(MarginStr :: [any()], |
350 |
|
Segments :: [[[any(), ...]]]) -> [[[any()], ...]]. |
351 |
|
mix_desc_segments(MarginString, Segments) -> |
352 |
112 |
[["\n", MarginString, Segment] || Segment <- Segments]. |
353 |
|
|
354 |
|
split_desc_segments(MaxL, Words) -> |
355 |
112 |
join(MaxL, Words). |
356 |
|
|
357 |
|
%% @doc Join words in a segment, but stop adding to a segment if adding this |
358 |
|
%% word would pass L |
359 |
|
-spec join(L :: number(), Words :: [nonempty_string()]) -> [[[any(), ...]], ...]. |
360 |
|
join(L, Words) -> |
361 |
112 |
join(L, Words, 0, [], []). |
362 |
|
|
363 |
|
|
364 |
|
-spec join(L :: number(), |
365 |
|
Words :: [nonempty_string()], |
366 |
|
LenLastSeg :: non_neg_integer(), |
367 |
|
LastSeg :: [nonempty_string()], |
368 |
|
ResSeg :: [[[any(), ...]]] ) -> [[[any(), ...]], ...]. |
369 |
|
join(_L, [], _LenLastSeg, LastSeg, ResSeg) -> |
370 |
112 |
ResSeg2 = [lists:reverse(LastSeg) | ResSeg], |
371 |
112 |
lists:reverse(ResSeg2); |
372 |
|
join(L, [Word | Words], LenLastSeg, LastSeg, ResSeg) -> |
373 |
499 |
LWord = length(Word), |
374 |
499 |
case LWord + LenLastSeg < L of |
375 |
|
true -> |
376 |
|
%% This word fits in the last segment |
377 |
|
%% If this word ends with "\n", reset column counter |
378 |
490 |
case string:str(Word, "\n") of |
379 |
|
0 -> |
380 |
490 |
join(L, Words, LenLastSeg+LWord+1, [" ", Word | LastSeg], ResSeg); |
381 |
|
_ -> |
382 |
:-( |
join(L, Words, LWord+1, [" ", Word | LastSeg], ResSeg) |
383 |
|
end; |
384 |
|
false -> |
385 |
9 |
join(L, Words, LWord, [" ", Word], [lists:reverse(LastSeg) | ResSeg]) |
386 |
|
end. |
387 |
|
|
388 |
|
-spec format_command_lines(CALD :: [{[any()], [any()], number(), _}, ...], |
389 |
|
MaxCmdLen :: integer(), |
390 |
|
MaxC :: integer(), |
391 |
|
ShCode :: boolean()) -> [[any(), ...], ...]. |
392 |
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode) when MaxC - MaxCmdLen < 40 -> |
393 |
|
% Long mode |
394 |
:-( |
lists:map( |
395 |
|
fun({Cmd, Args, _CmdArgsL, Desc}) -> |
396 |
:-( |
DescFmt = prepare_description(8, MaxC, Desc), |
397 |
:-( |
["\n ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], "\n", " ", |
398 |
|
DescFmt, "\n"] |
399 |
|
end, CALD); |
400 |
|
|
401 |
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode) -> |
402 |
|
% Dual mode |
403 |
17 |
lists:map( |
404 |
|
fun({Cmd, Args, CmdArgsL, Desc}) -> |
405 |
112 |
DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc), |
406 |
112 |
[" ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], |
407 |
|
string:chars(?ASCII_SPACE_CHARACTER, MaxCmdLen - CmdArgsL + 1), |
408 |
|
DescFmt, "\n"] |
409 |
|
end, CALD). |
410 |
|
|
411 |
|
%%----------------------------- |
412 |
|
%% Print usage command |
413 |
|
%%----------------------------- |
414 |
|
|
415 |
|
get_mongoose_status() -> |
416 |
2 |
case lists:keyfind(mongooseim, 1, application:which_applications()) of |
417 |
|
false -> |
418 |
:-( |
not_running; |
419 |
|
{_, _, Version} -> |
420 |
2 |
{running, mongooseim, Version} |
421 |
|
end. |
422 |
|
|
423 |
|
get_uptime() -> |
424 |
2 |
{MilliSeconds, _} = erlang:statistics(wall_clock), |
425 |
2 |
{D, {H, M, S}} = calendar:seconds_to_daystime(MilliSeconds div 1000), |
426 |
2 |
{uptime, [D, H, M, S]}. |
427 |
|
|
428 |
|
get_dist_proto() -> |
429 |
|
%% See kernel/src/net_kernel.erl |
430 |
2 |
case init:get_argument(proto_dist) of |
431 |
:-( |
{ok, [Proto]} -> Proto; |
432 |
2 |
_ -> "inet_tcp" |
433 |
|
end. |
434 |
|
|
435 |
|
%%----------------------------- |
436 |
|
%% Cluster management commands |
437 |
|
%%----------------------------- |
438 |
|
|
439 |
|
join_cluster(NodeString) -> |
440 |
5 |
handle_cluster_operation(join_cluster, [NodeString]). |
441 |
|
|
442 |
|
leave_cluster() -> |
443 |
6 |
handle_cluster_operation(leave_cluster, []). |
444 |
|
|
445 |
|
remove_from_cluster(NodeString) -> |
446 |
1 |
handle_cluster_operation(remove_from_cluster, [NodeString]). |
447 |
|
|
448 |
|
handle_cluster_operation(Operation, Args) -> |
449 |
12 |
case apply(mongoose_server_api, Operation, Args) of |
450 |
|
{ok, Result} -> |
451 |
9 |
?PRINT("~s\n", [Result]), |
452 |
9 |
?STATUS_SUCCESS; |
453 |
|
{_, Result} -> |
454 |
3 |
?PRINT("Error: \"~s\"\n", [Result]), |
455 |
3 |
?STATUS_ERROR |
456 |
|
end. |
457 |
|
|
458 |
|
cluster_command_usage() -> |
459 |
2 |
?PRINT("This command requires one argument: other node's name\n", []), |
460 |
2 |
?STATUS_ERROR. |