./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
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
:-(
cluster_command_without_arg();
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
:-(
cluster_command_without_arg();
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
142 %% @doc The arguments --long and --dual are not documented because they are
143 %% automatically selected depending in the number of columns of the shell
144 process(["help" | Mode]) ->
145 4 {MaxC, ShCode} = get_shell_info(),
146 4 case Mode of
147 [] ->
148 1 print_usage(dual, MaxC, ShCode),
149 1 ?STATUS_USAGE;
150 ["--dual"] ->
151 1 print_usage(dual, MaxC, ShCode),
152 1 ?STATUS_USAGE;
153 ["--long"] ->
154 1 print_usage(long, MaxC, ShCode),
155 1 ?STATUS_USAGE;
156 [_] ->
157 1 print_usage(dual, MaxC, ShCode),
158 1 ?STATUS_USAGE
159 end;
160 process(Args) ->
161 635 case mongoose_graphql_commands:process(Args) of
162 #{status := executed, result := Result} ->
163 621 handle_graphql_result(Result);
164 #{status := error, reason := no_args} = Ctx ->
165 1 print_usage(Ctx),
166 1 ?STATUS_USAGE;
167 #{status := error} = Ctx ->
168 11 ?PRINT(error_message(Ctx) ++ "\n\n", []),
169 11 print_usage(Ctx),
170 11 ?STATUS_ERROR;
171 #{status := usage} = Ctx ->
172 2 print_usage(Ctx),
173 2 ?STATUS_SUCCESS % not STATUS_USAGE, as that would tell the script to print general help
174 end.
175
176 -spec error_message(mongoose_graphql_commands:context()) -> iolist().
177 error_message(#{reason := unknown_command, command := Command}) ->
178 2 io_lib:format("Unknown command '~s'", [Command]);
179 error_message(#{reason := invalid_args}) ->
180 2 "Could not parse the command arguments";
181 error_message(#{reason := unknown_category}) ->
182 2 "Unknown category";
183 error_message(#{reason := {unknown_arg, ArgName}, command := Command}) ->
184 1 io_lib:format("Unknown argument '~s' for command '~s'", [ArgName, Command]);
185 error_message(#{reason := {invalid_arg_value, ArgName, ArgValue}, command := Command}) ->
186 1 io_lib:format("Invalid value '~s' of argument '~s' for command '~s'",
187 [ArgValue, ArgName, Command]);
188 error_message(#{reason := {missing_args, MissingArgs}, command := Command}) ->
189 3 io_lib:format("Missing mandatory arguments for command '~s': ~s",
190 [Command, ["'", lists:join("', '", MissingArgs), "'"]]).
191
192 -spec print_usage(mongoose_graphql_commands:context()) -> any().
193 print_usage(#{category := Category, command := Command, args_spec := ArgsSpec}) ->
194 8 print_usage_command(Category, Command, ArgsSpec);
195 print_usage(#{category := Category, commands := Commands}) ->
196 3 print_usage_category(Category, Commands);
197 print_usage(_) ->
198 3 {MaxC, ShCode} = get_shell_info(),
199 3 print_usage(dual, MaxC, ShCode).
200
201 handle_graphql_result({ok, Result}) ->
202 579 JSONResult = mongoose_graphql_response:term_to_pretty_json(Result),
203 579 ?PRINT("~s\n", [JSONResult]),
204 579 case Result of
205 222 #{errors := _} -> ?STATUS_ERROR;
206 357 _ -> ?STATUS_SUCCESS
207 end;
208 handle_graphql_result({error, Reason}) ->
209 44 {_Code, Error} = mongoose_graphql_errors:format_error(Reason),
210 44 JSONResult = jiffy:encode(#{errors => [Error]}, [pretty]),
211 44 ?PRINT("~s\n", [JSONResult]),
212 44 ?STATUS_ERROR.
213
214 %%-----------------------------
215 %% Format arguments
216 %%-----------------------------
217 format_status([{node, Node}, {internal_status, IS}, {provided_status, PS},
218 {mongoose_status, MS}, {os_pid, OSPid}, {uptime, UptimeHMS},
219 {dist_proto, DistProto}, {logs, LogFiles}]) ->
220 ( ["MongooseIM node ", ?a2l(Node), ":\n",
221 " operating system pid: ", OSPid, "\n",
222 " Erlang VM status: ", ?a2l(IS), " (of: starting | started | stopping)\n",
223 " boot script status: ", io_lib:format("~p", [PS]), "\n",
224 " version: ", case MS of
225 2 {running, App, Version} -> [Version, " (as ", ?a2l(App), ")"];
226
:-(
not_running -> "unavailable - neither ejabberd nor mongooseim is running"
227 end, "\n",
228 " uptime: ", io_lib:format(?TIME_HMS_FORMAT, UptimeHMS), "\n",
229 2 " distribution protocol: ", DistProto, "\n"] ++
230
:-(
[" logs: none - maybe enable logging to a file in app.config?\n" || LogFiles == [] ] ++
231 2 [" logs:\n" || LogFiles /= [] ] ++ [
232 4 [" ", LogFile, "\n"] || LogFile <- LogFiles ] ).
233
234 %%-----------------------------
235 %% Print help
236 %%-----------------------------
237
238 %% Bold
239 -define(B1, "\e[1m").
240 -define(B2, "\e[22m").
241 -define(B(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end).
242
243 %% Underline
244 -define(U1, "\e[4m").
245 -define(U2, "\e[24m").
246 -define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
247
248 print_usage() ->
249
:-(
{MaxC, ShCode} = get_shell_info(),
250
:-(
print_usage(dual, MaxC, ShCode).
251
252
253 -spec print_usage(dual | long, MaxC :: integer(), ShCode :: boolean()) -> ok.
254 print_usage(HelpMode, MaxC, ShCode) ->
255 7 ?PRINT(["Usage: ", ?B("mongooseimctl"), " [", ?U("category"), "] ", ?U("command"),
256 7 " [", ?U("arguments"), "]\n\n"
257 "Most MongooseIM commands are grouped into the following categories:\n"], []),
258 7 print_categories(HelpMode, MaxC, ShCode),
259 7 ?PRINT(["\nTo list the commands in a particular category:\n mongooseimctl ", ?U("category"),
260 "\n"], []),
261 7 ?PRINT(["\nThe following basic system management commands do not have a category:\n"], []),
262 7 print_usage_commands(HelpMode, MaxC, ShCode, basic_commands()).
263
264 -spec basic_commands() -> [cmd()].
265 basic_commands() ->
266 7 [{"status", [], "Get MongooseIM status"},
267 {"stop", [], "Stop MongooseIM"},
268 {"restart", [], "Restart MongooseIM"},
269 {"help", ["[--tags [tag] | com?*]"], "Show help for the deprecated commands"},
270 {"graphql", ["query"], "Execute GraphQL query or mutation"}].
271
272 -spec print_categories(dual | long, MaxC :: integer(), ShCode :: boolean()) -> ok.
273 print_categories(HelpMode, MaxC, ShCode) ->
274 7 SortedSpecs = lists:sort(maps:to_list(mongoose_graphql_commands:get_specs())),
275 7 Categories = [{binary_to_list(Category), [], binary_to_list(Desc)}
276 7 || {Category, #{desc := Desc}} <- SortedSpecs],
277 7 print_usage_commands(HelpMode, MaxC, ShCode, Categories).
278
279 -spec print_usage_category(mongoose_graphql_commands:category(),
280 mongoose_graphql_commands:command_map()) -> ok.
281 print_usage_category(Category, Commands) ->
282 3 {MaxC, ShCode} = get_shell_info(),
283 3 ?PRINT(["Usage: ", ?B("mongooseimctl"), " ", Category, " ", ?U("command"), " ", ?U("arguments"), "\n"
284 "\n"
285 "The following commands are available in the category '", Category, "':\n"], []),
286 3 CmdSpec = [{binary_to_list(Command), [], binary_to_list(Desc)}
287 3 || {Command, #{desc := Desc}} <- maps:to_list(Commands)],
288 3 print_usage_commands(dual, MaxC, ShCode, CmdSpec),
289 3 ?PRINT(["\nTo list the arguments for a particular command:\n"
290 3 " mongooseimctl ", Category, " ", ?U("command"), " --help", "\n"], []).
291
292 -spec print_usage_command(mongoose_graphql_commands:category(),
293 mongoose_graphql_commands:command(),
294 [mongoose_graphql_commands:arg_spec()]) -> ok.
295 print_usage_command(Category, Command, ArgsSpec) ->
296 8 {MaxC, ShCode} = get_shell_info(),
297 8 ?PRINT(["Usage: ", ?B("mongooseimctl"), " ", Category, " ", Command, " ", ?U("arguments"), "\n"
298 "\n",
299 8 "Each argument has the format: --", ?U("name"), " ", ?U("value"), "\n",
300 "Available arguments are listed below with the corresponding GraphQL types:\n"], []),
301 %% Reuse the function initially designed for printing commands for now
302 %% This will be replaced with new logic when old commands are dropped
303 8 Args = [{binary_to_list(Name), [], mongoose_graphql_commands:wrap_type(Wrap, Type)}
304 8 || #{name := Name, type := Type, wrap := Wrap} <- ArgsSpec],
305 8 print_usage_commands(dual, MaxC, ShCode, Args),
306 8 ?PRINT(["\nScalar values do not need quoting unless they contain special characters or spaces.\n"
307 "Complex input types are passed as JSON maps or lists, depending on the type.\n"
308 "When a type is followed by '!', the corresponding argument is required.\n"], []).
309
310 -spec print_usage_commands(HelpMode :: 'dual' | 'long',
311 MaxC :: integer(),
312 ShCode :: boolean(),
313 Commands :: [cmd(), ...]) -> 'ok'.
314 print_usage_commands(HelpMode, MaxC, ShCode, Commands) ->
315 25 CmdDescsSorted = lists:keysort(1, Commands),
316
317 %% What is the length of the largest command?
318 25 {CmdArgsLenDescsSorted, Lens} =
319 lists:mapfoldl(
320 fun({Cmd, Args, Desc}, Lengths) ->
321 215 Len =
322 length(Cmd) +
323 lists:foldl(fun(Arg, R) ->
324 14 R + 1 + length(Arg)
325 end,
326 0,
327 Args),
328 215 {{Cmd, Args, Len, Desc}, [Len | Lengths]}
329 end,
330 [],
331 CmdDescsSorted),
332 25 MaxCmdLen = case Lens of
333
:-(
[] -> 80;
334 25 _ -> lists:max(Lens)
335 end,
336
337 %% For each command in the list of commands
338 %% Convert its definition to a line
339 25 FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode, HelpMode),
340 25 ?PRINT([FmtCmdDescs], []).
341
342 %% @doc Get some info about the shell: how many columns of width and guess if
343 %% it supports text formatting codes.
344 -spec get_shell_info() -> {integer(), boolean()}.
345 get_shell_info() ->
346 18 case io:columns() of
347
:-(
{ok, C} -> {C-2, true};
348 18 {error, enotsup} -> {78, false}
349 end.
350
351 %% @doc Split this command description in several lines of proper length
352 -spec prepare_description(DescInit :: non_neg_integer(),
353 MaxC :: integer(),
354 Desc :: string()) -> [[[any()]], ...].
355 prepare_description(DescInit, MaxC, Desc) ->
356 215 Words = string:tokens(Desc, " \n"),
357 215 prepare_long_line(DescInit, MaxC, Words).
358
359 -spec prepare_long_line(DescInit :: non_neg_integer(),
360 MaxC :: integer(),
361 Words :: [nonempty_string()]
362 ) -> [[[any()]], ...].
363 prepare_long_line(DescInit, MaxC, Words) ->
364 215 MaxSegmentLen = MaxC - DescInit,
365 215 MarginString = lists:duplicate(DescInit, ?ASCII_SPACE_CHARACTER), % Put spaces
366 215 [FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words),
367 215 MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments),
368 215 [FirstSegment | MoreSegmentsMixed].
369
370 -spec mix_desc_segments(MarginStr :: [any()],
371 Segments :: [[[any(), ...]]]) -> [[[any()], ...]].
372 mix_desc_segments(MarginString, Segments) ->
373 215 [["\n", MarginString, Segment] || Segment <- Segments].
374
375 split_desc_segments(MaxL, Words) ->
376 215 join(MaxL, Words).
377
378 %% @doc Join words in a segment, but stop adding to a segment if adding this
379 %% word would pass L
380 -spec join(L :: number(), Words :: [nonempty_string()]) -> [[[any(), ...]], ...].
381 join(L, Words) ->
382 215 join(L, Words, 0, [], []).
383
384
385 -spec join(L :: number(),
386 Words :: [nonempty_string()],
387 LenLastSeg :: non_neg_integer(),
388 LastSeg :: [nonempty_string()],
389 ResSeg :: [[[any(), ...]]] ) -> [[[any(), ...]], ...].
390 join(_L, [], _LenLastSeg, LastSeg, ResSeg) ->
391 215 ResSeg2 = [lists:reverse(LastSeg) | ResSeg],
392 215 lists:reverse(ResSeg2);
393 join(L, [Word | Words], LenLastSeg, LastSeg, ResSeg) ->
394 873 LWord = length(Word),
395 873 case LWord + LenLastSeg < L of
396 true ->
397 %% This word fits in the last segment
398 %% If this word ends with "\n", reset column counter
399 864 case string:str(Word, "\n") of
400 0 ->
401 864 join(L, Words, LenLastSeg+LWord+1, [" ", Word | LastSeg], ResSeg);
402 _ ->
403
:-(
join(L, Words, LWord+1, [" ", Word | LastSeg], ResSeg)
404 end;
405 false ->
406 9 join(L, Words, LWord, [" ", Word], [lists:reverse(LastSeg) | ResSeg])
407 end.
408
409 -spec format_command_lines(CALD :: [{[any()], [any()], number(), _}, ...],
410 MaxCmdLen :: integer(),
411 MaxC :: integer(),
412 ShCode :: boolean(),
413 'dual' | 'long') -> [[any(), ...], ...].
414 format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual)
415 when MaxC - MaxCmdLen < 40 ->
416 %% If the space available for descriptions is too narrow, enforce long help mode
417
:-(
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long);
418 format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) ->
419 23 lists:map(
420 fun({Cmd, Args, CmdArgsL, Desc}) ->
421 190 DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc),
422 190 [" ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], string:chars(?ASCII_SPACE_CHARACTER, MaxCmdLen - CmdArgsL + 1),
423 DescFmt, "\n"]
424 end, CALD);
425 format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
426 2 lists:map(
427 fun({Cmd, Args, _CmdArgsL, Desc}) ->
428 25 DescFmt = prepare_description(8, MaxC, Desc),
429 25 ["\n ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], "\n", " ",
430 DescFmt, "\n"]
431 end, CALD).
432
433 %%-----------------------------
434 %% Print usage command
435 %%-----------------------------
436
437 get_mongoose_status() ->
438 2 case lists:keyfind(mongooseim, 1, application:which_applications()) of
439 false ->
440
:-(
not_running;
441 {_, _, Version} ->
442 2 {running, mongooseim, Version}
443 end.
444
445 get_uptime() ->
446 2 {MilliSeconds, _} = erlang:statistics(wall_clock),
447 2 {D, {H, M, S}} = calendar:seconds_to_daystime(MilliSeconds div 1000),
448 2 {uptime, [D, H, M, S]}.
449
450 get_dist_proto() ->
451 %% See kernel/src/net_kernel.erl
452 2 case init:get_argument(proto_dist) of
453
:-(
{ok, [Proto]} -> Proto;
454 2 _ -> "inet_tcp"
455 end.
456
457 %%-----------------------------
458 %% Cluster management commands
459 %%-----------------------------
460
461 join_cluster(NodeString) ->
462 5 handle_cluster_operation(join_cluster, [NodeString]).
463
464 leave_cluster() ->
465 6 handle_cluster_operation(leave_cluster, []).
466
467 remove_from_cluster(NodeString) ->
468 1 handle_cluster_operation(remove_from_cluster, [NodeString]).
469
470 handle_cluster_operation(Operation, Args) ->
471 12 case apply(mongoose_server_api, Operation, Args) of
472 {ok, Result} ->
473 9 ?PRINT("~s\n", [Result]),
474 9 ?STATUS_SUCCESS;
475 {_, Result} ->
476 3 ?PRINT("Error: \"~s\"\n", [Result]),
477 3 ?STATUS_ERROR
478 end.
479
480 cluster_command_without_arg() ->
481
:-(
?PRINT("You have to provide another node's name\n", []),
482
:-(
?STATUS_ERROR.
Line Hits Source