./ct_report/coverage/ejabberd_ctl.COVER.html

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 %%% TODO: Update the guide
40 %%% TODO: Mention this in the release notes
41 %%% Note: the commands with several words use now the underline: _
42 %%% It is still possible to call the commands with dash: -
43 %%% but this is deprecated, and may be removed in a future version.
44
45
46 -module(ejabberd_ctl).
47 -author('alexey@process-one.net').
48
49 -export([start/0,
50 init/0,
51 process/1,
52 process2/2,
53 register_commands/3,
54 unregister_commands/3]).
55
56 -ignore_xref([process/1, process2/2, register_commands/3, start/0, unregister_commands/3]).
57
58 -include("ejabberd_ctl.hrl").
59 -include("ejabberd_commands.hrl").
60 -include("mongoose.hrl").
61
62 -type format() :: integer | string | binary | {list, format()}.
63 -type format_type() :: binary() | string() | char().
64 -type cmd() :: {CallString :: string(), Args :: [string()], Desc :: string()}.
65
66 -define(ASCII_SPACE_CHARACTER, $\s).
67 -define(PRINT(Format, Args), io:format(lists:flatten(Format), Args)).
68 -define(TIME_HMS_FORMAT, "~B days ~2.10.0B:~2.10.0B:~2.10.0B").
69 -define(a2l(A), atom_to_list(A)).
70
71 %%-----------------------------
72 %% Module
73 %%-----------------------------
74
75 -spec start() -> none().
76 start() ->
77
:-(
case init:get_plain_arguments() of
78 [SNode | Args0] ->
79
:-(
Args = args_join_xml(args_join_strings(Args0)),
80
:-(
SNode1 = case string:tokens(SNode, "@") of
81 [_Node, _Server] ->
82
:-(
SNode;
83 _ ->
84
:-(
case net_kernel:longnames() of
85 true ->
86
:-(
SNode ++ "@" ++ inet_db:gethostname() ++
87 "." ++ inet_db:res_option(domain);
88 false ->
89
:-(
SNode ++ "@" ++ inet_db:gethostname();
90 _ ->
91
:-(
SNode
92 end
93 end,
94
:-(
Node = list_to_atom(SNode1),
95
:-(
Status = case rpc:call(Node, ?MODULE, process, [Args]) of
96 {badrpc, Reason} ->
97
:-(
?PRINT("Failed RPC connection to the node ~p: ~p~n",
98 [Node, Reason]),
99 %% TODO: show minimal start help
100
:-(
?STATUS_BADRPC;
101 S ->
102
:-(
S
103 end,
104
:-(
halt(Status);
105 _ ->
106
:-(
print_usage(),
107
:-(
halt(?STATUS_USAGE)
108 end.
109
110
111 -spec init() -> atom() | ets:tid().
112 init() ->
113 73 ets:new(ejabberd_ctl_cmds, [named_table, set, public]),
114 73 ets:new(ejabberd_ctl_host_cmds, [named_table, set, public]).
115
116
117 %%-----------------------------
118 %% mongooseimctl Command managment
119 %%-----------------------------
120
121 -spec register_commands(CmdDescs :: [tuple()] | tuple(),
122 Module :: atom(),
123 Function :: atom()) -> 'ok'.
124 register_commands(CmdDescs, Module, Function) ->
125
:-(
ets:insert(ejabberd_ctl_cmds, CmdDescs),
126
:-(
ejabberd_hooks:add(ejabberd_ctl_process, global, Module, Function, 50),
127
:-(
ok.
128
129
130 -spec unregister_commands(CmdDescs :: [any()],
131 Module :: atom(),
132 Function :: atom()) -> 'ok'.
133 unregister_commands(CmdDescs, Module, Function) ->
134
:-(
lists:foreach(fun(CmdDesc) ->
135
:-(
ets:delete_object(ejabberd_ctl_cmds, CmdDesc)
136 end, CmdDescs),
137
:-(
ejabberd_hooks:delete(ejabberd_ctl_process, global, Module, Function, 50),
138
:-(
ok.
139
140
141 %%-----------------------------
142 %% Process
143 %%-----------------------------
144
145 %% @doc The commands status, stop and restart are defined here to ensure
146 %% they are usable even if ejabberd is completely stopped.
147 -spec process(_) -> integer().
148 process(["status"]) ->
149
:-(
{InternalStatus, ProvidedStatus} = init:get_status(),
150
:-(
MongooseStatus = get_mongoose_status(),
151
:-(
?PRINT("~s", [format_status([{node, node()}, {internal_status, InternalStatus},
152 {provided_status, ProvidedStatus},
153 {mongoose_status, MongooseStatus},
154 {os_pid, os:getpid()}, get_uptime(),
155 {dist_proto, get_dist_proto()},
156 {logs, mongoose_logs:get_log_files()}])]),
157
:-(
case MongooseStatus of
158
:-(
not_running -> ?STATUS_ERROR;
159
:-(
{running, _, _Version} -> ?STATUS_SUCCESS
160 end;
161 process(["stop"]) ->
162 %%ejabberd_cover:stop(),
163
:-(
init:stop(),
164
:-(
?STATUS_SUCCESS;
165 process(["restart"]) ->
166
:-(
init:restart(),
167
:-(
?STATUS_SUCCESS;
168 process(["mnesia"]) ->
169
:-(
?PRINT("~p~n", [mnesia:system_info(all)]),
170
:-(
?STATUS_SUCCESS;
171 process(["mnesia", "info"]) ->
172
:-(
mnesia:info(),
173
:-(
?STATUS_SUCCESS;
174 process(["mnesia", Arg]) when is_list(Arg) ->
175
:-(
case catch mnesia:system_info(list_to_atom(Arg)) of
176
:-(
{'EXIT', Error} -> ?PRINT("Error: ~p~n", [Error]);
177
:-(
Return -> ?PRINT("~p~n", [Return])
178 end,
179
:-(
?STATUS_SUCCESS;
180 process(["graphql", Arg]) when is_list(Arg) ->
181 2 Doc = list_to_binary(Arg),
182 2 Ep = mongoose_graphql:get_endpoint(admin),
183 2 case mongoose_graphql:execute(Ep, undefined, Doc) of
184 {ok, Result} ->
185 1 PrettyResult = jiffy:encode(Result, [pretty]),
186 1 ?PRINT("~s\n", [PrettyResult]);
187 {error, _} = Err ->
188 1 ?PRINT("~p\n", [Err])
189 end,
190 2 ?STATUS_SUCCESS;
191 process(["graphql" | _]) ->
192 2 ?PRINT("This command requires one string type argument!\n", []),
193 2 ?STATUS_ERROR;
194
195 %% @doc The arguments --long and --dual are not documented because they are
196 %% automatically selected depending in the number of columns of the shell
197 process(["help" | Mode]) ->
198
:-(
{MaxC, ShCode} = get_shell_info(),
199
:-(
case Mode of
200 [] ->
201
:-(
print_usage(dual, MaxC, ShCode),
202
:-(
?STATUS_USAGE;
203 ["--dual"] ->
204
:-(
print_usage(dual, MaxC, ShCode),
205
:-(
?STATUS_USAGE;
206 ["--long"] ->
207
:-(
print_usage(long, MaxC, ShCode),
208
:-(
?STATUS_USAGE;
209 ["--tags"] ->
210
:-(
print_usage_tags(MaxC, ShCode),
211
:-(
?STATUS_SUCCESS;
212 ["--tags", Tag] ->
213
:-(
print_usage_tags(Tag, MaxC, ShCode),
214
:-(
?STATUS_SUCCESS;
215 ["help"] ->
216
:-(
print_usage_help(MaxC, ShCode),
217
:-(
?STATUS_SUCCESS;
218 [CmdString | _] ->
219
:-(
CmdStringU = re:replace(CmdString, "-", "_", [global, {return, list}]),
220
:-(
print_usage_commands(CmdStringU, MaxC, ShCode),
221
:-(
?STATUS_SUCCESS
222 end;
223 process(Args) ->
224 207 AccessCommands = get_accesscommands(),
225 207 {String, Code} = process2(Args, AccessCommands),
226 207 case String of
227 64 [] -> ok;
228 _ ->
229 143 io:format("~s~n", [String])
230 end,
231 207 Code.
232
233
234 -spec process2(Args :: [string()],
235 AccessCommands :: [ejabberd_commands:access_cmd()]
236 ) -> {String::string(), Code::integer()}.
237 process2(["--auth", User, Server, Pass | Args], AccessCommands) ->
238
:-(
process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass)},
239 AccessCommands);
240 process2(Args, AccessCommands) ->
241 207 process2(Args, noauth, AccessCommands).
242
243
244 %% @private
245 process2(Args, Auth, AccessCommands) ->
246 207 case try_run_ctp(Args, Auth, AccessCommands) of
247 {String, wrong_command_arguments} when is_list(String) ->
248
:-(
io:format(lists:flatten(["\n" | String]++["\n"])),
249
:-(
[CommandString | _] = Args,
250
:-(
process(["help" | [CommandString]]),
251
:-(
{lists:flatten(String), ?STATUS_ERROR};
252 {String, Code} when is_list(String) and is_integer(Code) ->
253 179 {lists:flatten(String), Code};
254 String when is_list(String) ->
255 28 {lists:flatten(String), ?STATUS_SUCCESS};
256 Other ->
257
:-(
{"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR}
258 end.
259
260
261 -spec get_accesscommands() -> [char() | tuple()].
262 get_accesscommands() ->
263 207 mongoose_config:get_opt(mongooseimctl_access_commands).
264
265
266 %%-----------------------------
267 %% Command calling
268 %%-----------------------------
269
270 -spec try_run_ctp(Args :: [string()],
271 Auth :: ejabberd_commands:auth(),
272 AccessCommands :: [ejabberd_commands:access_cmd()]
273 ) -> string() | integer() | {string(), integer()} | {string(), wrong_command_arguments}.
274 try_run_ctp(Args, Auth, AccessCommands) ->
275 207 try mongoose_hooks:ejabberd_ctl_process(false, Args) of
276 false when Args /= [] ->
277 207 try_call_command(Args, Auth, AccessCommands);
278 false ->
279
:-(
print_usage(),
280
:-(
{"", ?STATUS_USAGE};
281 Status ->
282
:-(
{"", Status}
283 catch
284 exit:Why ->
285
:-(
print_usage(),
286
:-(
{io_lib:format("Error in mongooseimctl process: ~p", [Why]), ?STATUS_USAGE};
287 Error:Why ->
288 %% In this case probably ejabberd is not started, so let's show Status
289
:-(
process(["status"]),
290
:-(
?PRINT("~n", []),
291
:-(
{io_lib:format("Error in mongooseimctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
292 end.
293
294
295 -spec try_call_command(Args :: [string()],
296 Auth :: ejabberd_commands:auth(),
297 AccessCommands :: [ejabberd_commands:access_cmd()]
298 ) -> string() | integer() | {string(), integer()} | {string(), wrong_command_arguments}.
299 try_call_command(Args, Auth, AccessCommands) ->
300 207 try call_command(Args, Auth, AccessCommands) of
301 {error, command_unknown} ->
302
:-(
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
303 Res ->
304 207 Res
305 catch
306 A:Why:Stack ->
307
:-(
{io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR}
308 end.
309
310
311 -spec call_command(Args :: [string()],
312 Auth :: ejabberd_commands:auth(),
313 AccessCommands :: [ejabberd_commands:access_cmd()]
314 ) -> string() | integer() | {string(), integer()}
315 | {string(), wrong_command_arguments} | {error, command_unknown}.
316 call_command([CmdString | Args], Auth, AccessCommands) ->
317 207 CmdStringU = re:replace(CmdString, "-", "_", [global, {return, list}]),
318 207 Command = list_to_atom(CmdStringU),
319 207 case ejabberd_commands:get_command_format(Command) of
320 {error, command_unknown} ->
321
:-(
{error, command_unknown};
322 {ArgsFormat, ResultFormat} ->
323 207 case (catch format_args(Args, ArgsFormat)) of
324 ArgsFormatted when is_list(ArgsFormatted) ->
325 207 Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command,
326 ArgsFormatted),
327 207 format_result(Result, ResultFormat);
328 {'EXIT', {function_clause, [{lists, zip, [A1, A2], _FileInfo} | _]}} ->
329
:-(
{NumCompa, TextCompa} =
330 case {length(A1), length(A2)} of
331
:-(
{L1, L2} when L1 < L2 -> {L2-L1, "less argument"};
332
:-(
{L1, L2} when L1 > L2 -> {L1-L2, "more argument"}
333 end,
334
:-(
{io_lib:format("Error: the command ~p requires ~p ~s.",
335 [CmdString, NumCompa, TextCompa]),
336 wrong_command_arguments}
337 end
338 end.
339
340
341 %%-----------------------------
342 %% Format arguments
343 %%-----------------------------
344
345 %% @private
346 -spec args_join_xml([string()]) -> [string()].
347 args_join_xml([]) ->
348
:-(
[];
349 args_join_xml([ [ $< | _ ] = Arg | RArgs ]) ->
350
:-(
case bal(Arg, $<, $>) of
351 true ->
352
:-(
[Arg | args_join_xml(RArgs)];
353 false ->
354
:-(
[NextArg | RArgs1] = RArgs,
355
:-(
args_join_xml([Arg ++ " " ++ NextArg | RArgs1])
356 end;
357 args_join_xml([ Arg | RArgs ]) ->
358
:-(
[ Arg | args_join_xml(RArgs) ].
359
360
361 %% @private
362 -spec args_join_strings([string()]) -> [string()].
363 args_join_strings([]) ->
364
:-(
[];
365 args_join_strings([ "\"", NextArg | RArgs ]) ->
366
:-(
args_join_strings([ "\"" ++ NextArg | RArgs ]);
367 args_join_strings([ [ $" | _ ] = Arg | RArgs ]) ->
368
:-(
case lists:nthtail(length(Arg)-2, Arg) of
369 [C1, $"] when C1 /= ?ASCII_SPACE_CHARACTER ->
370
:-(
[ string:substr(Arg, 2, length(Arg)-2) | args_join_strings(RArgs) ];
371 _ ->
372
:-(
[NextArg | RArgs1] = RArgs,
373
:-(
args_join_strings([Arg ++ " " ++ NextArg | RArgs1])
374 end;
375 args_join_strings([ Arg | RArgs ]) ->
376
:-(
[ Arg | args_join_strings(RArgs) ].
377
378
379 %% @private
380 -spec bal(string(), char(), char()) -> boolean().
381 bal(String, Left, Right) ->
382
:-(
bal(String, Left, Right, 0).
383
384
385 %% @private
386 -spec bal(string(), L :: char(), R :: char(), Bal :: integer()) -> boolean().
387 bal([], _Left, _Right, Bal) ->
388
:-(
Bal == 0;
389 bal([?ASCII_SPACE_CHARACTER, _NextChar | T], Left, Right, Bal) ->
390
:-(
bal(T, Left, Right, Bal);
391 bal([Left | T], Left, Right, Bal) ->
392
:-(
bal(T, Left, Right, Bal-1);
393 bal([Right | T], Left, Right, Bal) ->
394
:-(
bal(T, Left, Right, Bal+1);
395 bal([_C | T], Left, Right, Bal) ->
396
:-(
bal(T, Left, Right, Bal).
397
398
399 %% @private
400 -spec format_args(Args :: [any()],
401 ArgsFormat :: [format()]) -> [any()].
402 format_args(Args, ArgsFormat) ->
403 207 lists:foldl(
404 fun({{_ArgName, ArgFormat}, Arg}, Res) ->
405 511 Formatted = format_arg(Arg, ArgFormat),
406 511 Res ++ [Formatted]
407 end,
408 [],
409 lists:zip(ArgsFormat, Args)).
410
411
412 %% @private
413 -spec format_arg(string(), format()) -> format_type().
414 format_arg(Arg, integer) ->
415 25 format_arg2(Arg, "~d");
416 format_arg("", string) ->
417 6 "";
418 format_arg(Arg, string) ->
419 481 NumChars = integer_to_list(string:len(Arg)),
420 481 Parse = "~" ++ NumChars ++ "c",
421 481 format_arg2(Arg, Parse);
422 format_arg(Arg, binary) ->
423 459 list_to_binary(format_arg(Arg, string));
424 format_arg(Arg, {list, Type}) ->
425 1 [format_arg(Token, Type) || Token <- string:tokens(Arg, ";")].
426
427
428 %% @private
429 -spec format_arg2(Arg :: string(),
430 Parse :: nonempty_string()
431 ) -> [[any()] | char()] | char().
432 format_arg2(Arg, Parse)->
433 506 {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg),
434 506 Arg2.
435
436 %%-----------------------------
437 %% Format result
438 %%-----------------------------
439
440 format_error(Error) ->
441 27 try
442 27 io_lib:format("\"~ts\"", [Error])
443 catch _ ->
444
:-(
io_lib:format("~p", [Error])
445 end.
446
447 -spec format_result(In :: tuple() | atom() | integer() | string() | binary(),
448 {_, 'atom'|'integer'|'string'|'binary'}
449 ) -> string() | {string(), _}.
450 format_result({error, Error}, _) ->
451 27 {io_lib:format("Error: ~ts", [format_error(Error)]), make_status(error)};
452 format_result(Atom, {_Name, atom}) ->
453
:-(
io_lib:format("~p", [Atom]);
454 format_result(Int, {_Name, integer}) ->
455 37 io_lib:format("~p", [Int]);
456 format_result(String, {_Name, string}) ->
457 45 io_lib:format("~s", [String]);
458 format_result(Binary, {_Name, binary}) ->
459 34 io_lib:format("~s", [Binary]);
460 format_result(Code, {_Name, rescode}) ->
461 46 {"", make_status(Code)};
462 format_result({Code, Text}, {_Name, restuple}) ->
463 106 {io_lib:format("~s", [Text]), make_status(Code)};
464 %% The result is a list of something: [something()]
465 format_result([], {_Name, {list, _ElementsDef}}) ->
466 2 "";
467 format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) ->
468 %% Start formatting the first element
469 11 [format_result(FirstElement, ElementsDef) |
470 %% If there are more elements, put always first a newline character
471 lists:map(
472 fun(Element) ->
473 20 ["\n" | format_result(Element, ElementsDef)]
474 end,
475 Elements)];
476 %% The result is a tuple with several elements: {something1(), something2(), ...}
477 %% NOTE: the elements in the tuple are separated with tabular characters,
478 %% if a string is empty, it will be difficult to notice in the shell,
479 %% maybe a different separation character should be used, like ;;?
480 format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) ->
481 14 ElementsList = tuple_to_list(ElementsTuple),
482 14 [{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef),
483 14 [format_result(FirstE, FirstD) |
484 lists:map(
485 fun({Element, ElementDef}) ->
486 70 ["\t" | format_result(Element, ElementDef)]
487 end,
488 ElementsAndDef)].
489
490
491 -spec make_status(ok | true | _) -> 0 | 1.
492 135 make_status(ok) -> ?STATUS_SUCCESS;
493
:-(
make_status(true) -> ?STATUS_SUCCESS;
494 44 make_status(_Error) -> ?STATUS_ERROR.
495
496
497 -spec get_list_commands()
498 -> [{Call :: string(), Args :: [string()], Desc :: string()}].
499 get_list_commands() ->
500
:-(
try ejabberd_commands:list_commands() of
501 Commands ->
502
:-(
[tuple_command_help(Command)
503
:-(
|| {N, _, _}=Command <- Commands,
504 %% Don't show again those commands, because they are already
505 %% announced by ejabberd_ctl itself
506
:-(
N /= status, N /= stop, N /= restart]
507 catch
508 exit:_ ->
509
:-(
[]
510 end.
511
512
513 -spec tuple_command_help(ejabberd_commands:list_cmd()) -> cmd().
514 tuple_command_help({Name, Args, Desc}) ->
515
:-(
Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args],
516
:-(
CallString = atom_to_list(Name),
517
:-(
{CallString, Arguments, Desc}.
518
519
520 -spec get_list_ctls() -> [cmd()].
521 get_list_ctls() ->
522
:-(
case catch ets:tab2list(ejabberd_ctl_cmds) of
523
:-(
{'EXIT', _} -> [];
524
:-(
Cs -> [{NameArgs, [], Desc} || {NameArgs, Desc} <- Cs]
525 end.
526
527 format_status([{node, Node}, {internal_status, IS}, {provided_status, PS},
528 {mongoose_status, MS}, {os_pid, OSPid}, {uptime, UptimeHMS},
529 {dist_proto, DistProto}, {logs, LogFiles}]) ->
530 ( ["MongooseIM node ", ?a2l(Node), ":\n",
531 " operating system pid: ", OSPid, "\n",
532 " Erlang VM status: ", ?a2l(IS), " (of: starting | started | stopping)\n",
533 " boot script status: ", io_lib:format("~p", [PS]), "\n",
534 " version: ", case MS of
535
:-(
{running, App, Version} -> [Version, " (as ", ?a2l(App), ")"];
536
:-(
not_running -> "unavailable - neither ejabberd nor mongooseim is running"
537 end, "\n",
538 " uptime: ", io_lib:format(?TIME_HMS_FORMAT, UptimeHMS), "\n",
539
:-(
" distribution protocol: ", DistProto, "\n"] ++
540
:-(
[" logs: none - maybe enable logging to a file in app.config?\n" || LogFiles == [] ] ++
541
:-(
[" logs:\n" || LogFiles /= [] ] ++ [
542
:-(
[" ", LogFile, "\n"] || LogFile <- LogFiles ] ).
543
544 %%-----------------------------
545 %% Print help
546 %%-----------------------------
547
548 %% Bold
549 -define(B1, "\e[1m").
550 -define(B2, "\e[22m").
551 -define(B(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end).
552
553 %% Underline
554 -define(U1, "\e[4m").
555 -define(U2, "\e[24m").
556 -define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
557
558 print_usage() ->
559
:-(
{MaxC, ShCode} = get_shell_info(),
560
:-(
print_usage(dual, MaxC, ShCode).
561
562
563 -spec print_usage('dual' | 'long'
564 , MaxC :: integer()
565 , ShCode :: boolean()) -> 'ok'.
566 print_usage(HelpMode, MaxC, ShCode) ->
567
:-(
AllCommands =
568 [
569 {"status", [], "Get MongooseIM status"},
570 {"stop", [], "Stop MongooseIM"},
571 {"restart", [], "Restart MongooseIM"},
572 {"help", ["[--tags [tag] | com?*]"], "Show help (try: mongooseimctl help help)"},
573 {"mnesia", ["[info]"], "show information of Mnesia system"},
574 {"graphql", ["query"], "Execute graphql query or mutation"}] ++
575 get_list_commands() ++
576 get_list_ctls(),
577
578
:-(
?PRINT(
579
:-(
["Usage: ", ?B("mongooseimctl"), " [--node ", ?U("nodename"), "] [--auth ",
580
:-(
?U("user"), " ", ?U("host"), " ", ?U("password"), "] ",
581
:-(
?U("command"), " [", ?U("options"), "]\n"
582 "\n"
583 "Available commands in this MongooseIM node:\n"], []),
584
:-(
print_usage_commands(HelpMode, MaxC, ShCode, AllCommands),
585
:-(
?PRINT(
586 ["\n"
587 "Examples:\n"
588 " mongooseimctl restart\n"
589 " mongooseimctl --node mongooseim@host restart\n"],
590 []).
591
592
593 -spec print_usage_commands(HelpMode :: 'dual' | 'long',
594 MaxC :: integer(),
595 ShCode :: boolean(),
596 Commands :: [cmd(), ...]) -> 'ok'.
597 print_usage_commands(HelpMode, MaxC, ShCode, Commands) ->
598
:-(
CmdDescsSorted = lists:keysort(1, Commands),
599
600 %% What is the length of the largest command?
601
:-(
{CmdArgsLenDescsSorted, Lens} =
602 lists:mapfoldl(
603 fun({Cmd, Args, Desc}, Lengths) ->
604
:-(
Len =
605 length(Cmd) +
606 lists:foldl(fun(Arg, R) ->
607
:-(
R + 1 + length(Arg)
608 end,
609 0,
610 Args),
611
:-(
{{Cmd, Args, Len, Desc}, [Len | Lengths]}
612 end,
613 [],
614 CmdDescsSorted),
615
:-(
MaxCmdLen = case Lens of
616
:-(
[] -> 80;
617
:-(
_ -> lists:max(Lens)
618 end,
619
620 %% For each command in the list of commands
621 %% Convert its definition to a line
622
:-(
FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode, HelpMode),
623
624
:-(
?PRINT([FmtCmdDescs], []).
625
626
627 %% @doc Get some info about the shell: how many columns of width and guess if
628 %% it supports text formatting codes.
629 -spec get_shell_info() -> {integer(), boolean()}.
630 get_shell_info() ->
631
:-(
case io:columns() of
632
:-(
{ok, C} -> {C-2, true};
633
:-(
{error, enotsup} -> {78, false}
634 end.
635
636
637 %% @doc Split this command description in several lines of proper length
638 -spec prepare_description(DescInit :: non_neg_integer(),
639 MaxC :: integer(),
640 Desc :: string()) -> [[[any()]], ...].
641 prepare_description(DescInit, MaxC, Desc) ->
642
:-(
Words = string:tokens(Desc, " "),
643
:-(
prepare_long_line(DescInit, MaxC, Words).
644
645
646 -spec prepare_long_line(DescInit :: non_neg_integer(),
647 MaxC :: integer(),
648 Words :: [nonempty_string()]
649 ) -> [[[any()]], ...].
650 prepare_long_line(DescInit, MaxC, Words) ->
651
:-(
MaxSegmentLen = MaxC - DescInit,
652
:-(
MarginString = lists:duplicate(DescInit, ?ASCII_SPACE_CHARACTER), % Put spaces
653
:-(
[FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words),
654
:-(
MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments),
655
:-(
[FirstSegment | MoreSegmentsMixed].
656
657
658 -spec mix_desc_segments(MarginStr :: [any()],
659 Segments :: [[[any(), ...]]]) -> [[[any()], ...]].
660 mix_desc_segments(MarginString, Segments) ->
661
:-(
[["\n", MarginString, Segment] || Segment <- Segments].
662
663 split_desc_segments(MaxL, Words) ->
664
:-(
join(MaxL, Words).
665
666
667 %% @doc Join words in a segment, but stop adding to a segment if adding this
668 %% word would pass L
669 -spec join(L :: number(), Words :: [nonempty_string()]) -> [[[any(), ...]], ...].
670 join(L, Words) ->
671
:-(
join(L, Words, 0, [], []).
672
673
674 -spec join(L :: number(),
675 Words :: [nonempty_string()],
676 LenLastSeg :: non_neg_integer(),
677 LastSeg :: [nonempty_string()],
678 ResSeg :: [[[any(), ...]]] ) -> [[[any(), ...]], ...].
679 join(_L, [], _LenLastSeg, LastSeg, ResSeg) ->
680
:-(
ResSeg2 = [lists:reverse(LastSeg) | ResSeg],
681
:-(
lists:reverse(ResSeg2);
682 join(L, [Word | Words], LenLastSeg, LastSeg, ResSeg) ->
683
:-(
LWord = length(Word),
684
:-(
case LWord + LenLastSeg < L of
685 true ->
686 %% This word fits in the last segment
687 %% If this word ends with "\n", reset column counter
688
:-(
case string:str(Word, "\n") of
689 0 ->
690
:-(
join(L, Words, LenLastSeg+LWord+1, [" ", Word | LastSeg], ResSeg);
691 _ ->
692
:-(
join(L, Words, LWord+1, [" ", Word | LastSeg], ResSeg)
693 end;
694 false ->
695
:-(
join(L, Words, LWord, [" ", Word], [lists:reverse(LastSeg) | ResSeg])
696 end.
697
698
699 -spec format_command_lines(CALD :: [{[any()], [any()], number(), _}, ...],
700 MaxCmdLen :: integer(),
701 MaxC :: integer(),
702 ShCode :: boolean(),
703 'dual' | 'long') -> [[any(), ...], ...].
704 format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual)
705 when MaxC - MaxCmdLen < 40 ->
706 %% If the space available for descriptions is too narrow, enforce long help mode
707
:-(
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long);
708 format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) ->
709
:-(
lists:map(
710 fun({Cmd, Args, CmdArgsL, Desc}) ->
711
:-(
DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc),
712
:-(
[" ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], string:chars(?ASCII_SPACE_CHARACTER, MaxCmdLen - CmdArgsL + 1),
713 DescFmt, "\n"]
714 end, CALD);
715 format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
716
:-(
lists:map(
717 fun({Cmd, Args, _CmdArgsL, Desc}) ->
718
:-(
DescFmt = prepare_description(8, MaxC, Desc),
719
:-(
["\n ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], "\n", " ",
720 DescFmt, "\n"]
721 end, CALD).
722
723
724 %%-----------------------------
725 %% Print Tags
726 %%-----------------------------
727
728 print_usage_tags(MaxC, ShCode) ->
729
:-(
?PRINT("Available tags and commands:", []),
730
:-(
TagsCommands = ejabberd_commands:get_tags_commands(),
731
:-(
lists:foreach(
732 fun({Tag, Commands} = _TagCommands) ->
733
:-(
?PRINT(["\n\n ", ?B(Tag), "\n "], []),
734
:-(
Words = lists:sort(Commands),
735
:-(
Desc = prepare_long_line(5, MaxC, Words),
736
:-(
?PRINT(Desc, [])
737 end,
738 TagsCommands),
739
:-(
?PRINT("\n\n", []).
740
741
742 print_usage_tags(Tag, MaxC, ShCode) ->
743
:-(
?PRINT(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
744
:-(
HelpMode = long,
745
:-(
TagsCommands = ejabberd_commands:get_tags_commands(),
746
:-(
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
747
:-(
{value, {Tag, CNs}} -> CNs;
748
:-(
false -> []
749 end,
750
:-(
CommandsList = lists:map(
751 fun(NameString) ->
752
:-(
C = ejabberd_commands:get_command_definition(list_to_atom(NameString)),
753
:-(
#ejabberd_commands{name = Name,
754 args = Args,
755 desc = Desc} = C,
756
:-(
tuple_command_help({Name, Args, Desc})
757 end,
758 CommandsNames),
759
:-(
print_usage_commands(HelpMode, MaxC, ShCode, CommandsList),
760
:-(
?PRINT("\n", []).
761
762
763 %%-----------------------------
764 %% Print usage of 'help' command
765 %%-----------------------------
766
767 print_usage_help(MaxC, ShCode) ->
768
:-(
LongDesc =
769 ["The special 'help' mongooseimctl command provides help of MongooseIM commands.\n\n"
770
:-(
"The format is:\n ", ?B("mongooseimctl"), " ", ?B("help"), " [", ?B("--tags"), " ", ?U("[tag]"), " | ", ?U("com?*"), "]\n\n"
771 "The optional arguments:\n"
772
:-(
" ", ?B("--tags"), " Show all tags and the names of commands in each tag\n"
773
:-(
" ", ?B("--tags"), " ", ?U("tag"), " Show description of commands in this tag\n"
774
:-(
" ", ?U("command"), " Show detailed description of the command\n"
775
:-(
" ", ?U("com?*"), " Show detailed description of commands that match this glob.\n"
776 " You can use ? to match a simple character, \n"
777 " and * to match several characters.\n"
778 "\n",
779 "Some example usages:\n",
780 " mongooseimctl help\n",
781 " mongooseimctl help --tags\n",
782 " mongooseimctl help --tags accounts\n",
783 " mongooseimctl help register\n",
784 " mongooseimctl help regist*\n",
785 "\n",
786 "Please note that 'mongooseimctl help' shows all MongooseIM commands, \n",
787 "even those that cannot be used in the shell with mongooseimctl.\n",
788 "Those commands can be identified because the description starts with: *"],
789
:-(
ArgsDef = [],
790
:-(
C = #ejabberd_commands{
791 name = help,
792 desc = "Show help of MongooseIM commands",
793 longdesc = lists:flatten(LongDesc),
794 args = ArgsDef,
795 module = none,
796 function = none,
797 result = {help, string}},
798
:-(
print_usage_command("help", C, MaxC, ShCode).
799
800
801 %%-----------------------------
802 %% Print usage command
803 %%-----------------------------
804
805 -spec print_usage_commands(CmdSubString :: string(), MaxC :: integer(), ShCode :: boolean()) -> ok.
806 print_usage_commands(CmdSubString, MaxC, ShCode) ->
807 %% Get which command names match this substring
808
:-(
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()],
809
:-(
Cmds = filter_commands(AllCommandsNames, CmdSubString),
810
:-(
case Cmds of
811
:-(
[] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]);
812
:-(
_ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode)
813 end.
814
815
816 print_usage_commands2(Cmds, MaxC, ShCode) ->
817 %% Then for each one print it
818
:-(
lists:mapfoldl(
819 fun(Cmd, Remaining) ->
820
:-(
print_usage_command(Cmd, MaxC, ShCode),
821
:-(
case Remaining > 1 of
822
:-(
true -> ?PRINT([" ", lists:duplicate(MaxC, 126), " \n"], []);
823
:-(
false -> ok
824 end,
825
:-(
{ok, Remaining-1}
826 end,
827 length(Cmds),
828 Cmds).
829
830
831 filter_commands(All, SubString) ->
832
:-(
case lists:member(SubString, All) of
833
:-(
true -> [SubString];
834
:-(
false -> filter_commands_regexp(All, SubString)
835 end.
836
837
838 filter_commands_regexp(All, Glob) ->
839
:-(
RegExp = xmerl_regexp:sh_to_awk(Glob),
840
:-(
lists:filter(
841 fun(Command) ->
842
:-(
case re:run(Command, RegExp, [{capture, none}]) of
843 match ->
844
:-(
true;
845 nomatch ->
846
:-(
false
847 end
848 end,
849 All).
850
851
852 -spec print_usage_command(Cmd :: string(), MaxC :: integer(), ShCode :: boolean()) -> ok.
853 print_usage_command(Cmd, MaxC, ShCode) ->
854
:-(
Name = list_to_atom(Cmd),
855
:-(
case ejabberd_commands:get_command_definition(Name) of
856 command_not_found ->
857
:-(
io:format("Error: command ~p not known.~n", [Cmd]);
858 C ->
859
:-(
print_usage_command(Cmd, C, MaxC, ShCode)
860 end.
861
862
863 print_usage_command(Cmd, C, MaxC, ShCode) ->
864
:-(
#ejabberd_commands{
865 tags = TagsAtoms,
866 desc = Desc,
867 longdesc = LongDesc,
868 args = ArgsDef,
869 result = ResultDef} = C,
870
871
:-(
NameFmt = [" ", ?B("Command Name"), ": ", Cmd, "\n"],
872
873 %% Initial indentation of result is 13 = length(" Arguments: ")
874
:-(
Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef],
875
:-(
ArgsMargin = lists:duplicate(13, ?ASCII_SPACE_CHARACTER),
876
:-(
ArgsListFmt = case Args of
877
:-(
[] -> "\n";
878
:-(
_ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args]
879 end,
880
:-(
ArgsFmt = [" ", ?B("Arguments"), ": ", ArgsListFmt],
881
882 %% Initial indentation of result is 11 = length(" Returns: ")
883
:-(
ResultFmt = format_usage_ctype(ResultDef, 11),
884
:-(
ReturnsFmt = [" ", ?B("Returns"), ": ", ResultFmt],
885
886
:-(
TagsFmt = [" ", ?B("Tags"), ": ", prepare_long_line(8, MaxC, [atom_to_list(TagA) || TagA <- TagsAtoms])],
887
888
:-(
DescFmt = [" ", ?B("Description"), ": ", prepare_description(15, MaxC, Desc)],
889
890
:-(
LongDescFmt = case LongDesc of
891
:-(
"" -> "";
892
:-(
_ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"]
893 end,
894
895
:-(
?PRINT(["\n", NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, "\n\n", TagsFmt, "\n\n", DescFmt, "\n\n", LongDescFmt], []).
896
897
898 format_usage_ctype(Type, _Indentation)
899 when (Type==atom) or (Type==integer) or (Type==string) or (Type==rescode) or (Type==restuple) or (Type==binary)->
900
:-(
io_lib:format("~p", [Type]);
901 format_usage_ctype({Name, Type}, _Indentation)
902 when (Type==atom) or (Type==integer) or (Type==string) or (Type==rescode) or (Type==restuple) or (Type==binary)->
903
:-(
io_lib:format("~p::~p", [Name, Type]);
904 format_usage_ctype({Name, {list, ElementDef}}, Indentation) ->
905
:-(
NameFmt = atom_to_list(Name),
906
:-(
Indentation2 = Indentation + length(NameFmt) + 4,
907
:-(
ElementFmt = format_usage_ctype(ElementDef, Indentation2),
908
:-(
[NameFmt, "::[ ", ElementFmt, " ]"];
909 format_usage_ctype({Name, {tuple, ElementsDef}}, Indentation) ->
910
:-(
NameFmt = atom_to_list(Name),
911
:-(
Indentation2 = Indentation + length(NameFmt) + 4,
912
:-(
ElementsFmt = format_usage_tuple(ElementsDef, Indentation2),
913
:-(
[NameFmt, "::{ " | ElementsFmt].
914
915
916 format_usage_tuple([], _Indentation) ->
917
:-(
[];
918 format_usage_tuple([ElementDef], Indentation) ->
919
:-(
[format_usage_ctype(ElementDef, Indentation), " }"];
920 format_usage_tuple([ElementDef | ElementsDef], Indentation) ->
921
:-(
ElementFmt = format_usage_ctype(ElementDef, Indentation),
922
:-(
MarginString = lists:duplicate(Indentation, ?ASCII_SPACE_CHARACTER), % Put spaces
923
:-(
[ElementFmt, ", \n", MarginString, format_usage_tuple(ElementsDef, Indentation)].
924
925 get_mongoose_status() ->
926
:-(
case lists:keyfind(mongooseim, 1, application:which_applications()) of
927 false ->
928
:-(
not_running;
929 {_, _, Version} ->
930
:-(
{running, mongooseim, Version}
931 end.
932
933 get_uptime() ->
934
:-(
{MilliSeconds, _} = erlang:statistics(wall_clock),
935
:-(
{D, {H, M, S}} = calendar:seconds_to_daystime(MilliSeconds div 1000),
936
:-(
{uptime, [D, H, M, S]}.
937
938 get_dist_proto() ->
939 %% See kernel/src/net_kernel.erl
940
:-(
case init:get_argument(proto_dist) of
941
:-(
{ok, [Proto]} -> Proto;
942
:-(
_ -> "inet_tcp"
943 end.
944
Line Hits Source