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