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