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