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 |
80 |
ets:new(ejabberd_ctl_cmds, [named_table, set, public]), |
114 |
80 |
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 |
188 |
AccessCommands = get_accesscommands(), |
225 |
188 |
{String, Code} = process2(Args, AccessCommands), |
226 |
188 |
case String of |
227 |
44 |
[] -> ok; |
228 |
|
_ -> |
229 |
144 |
io:format("~s~n", [String]) |
230 |
|
end, |
231 |
188 |
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 |
188 |
process2(Args, noauth, AccessCommands). |
242 |
|
|
243 |
|
|
244 |
|
%% @private |
245 |
|
process2(Args, Auth, AccessCommands) -> |
246 |
188 |
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 |
153 |
{lists:flatten(String), Code}; |
254 |
|
String when is_list(String) -> |
255 |
35 |
{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 |
188 |
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 |
188 |
try mongoose_hooks:ejabberd_ctl_process(false, Args) of |
276 |
|
false when Args /= [] -> |
277 |
188 |
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 |
188 |
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 |
188 |
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 |
188 |
CmdStringU = re:replace(CmdString, "-", "_", [global, {return, list}]), |
318 |
188 |
Command = list_to_atom(CmdStringU), |
319 |
188 |
case ejabberd_commands:get_command_format(Command) of |
320 |
|
{error, command_unknown} -> |
321 |
:-( |
{error, command_unknown}; |
322 |
|
{ArgsFormat, ResultFormat} -> |
323 |
188 |
case (catch format_args(Args, ArgsFormat)) of |
324 |
|
ArgsFormatted when is_list(ArgsFormatted) -> |
325 |
188 |
Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, |
326 |
|
ArgsFormatted), |
327 |
188 |
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 |
188 |
lists:foldl( |
404 |
|
fun({{_ArgName, ArgFormat}, Arg}, Res) -> |
405 |
575 |
Formatted = format_arg(Arg, ArgFormat), |
406 |
575 |
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 |
33 |
format_arg2(Arg, "~d"); |
416 |
|
format_arg("", string) -> |
417 |
8 |
""; |
418 |
|
format_arg(Arg, string) -> |
419 |
535 |
NumChars = integer_to_list(string:len(Arg)), |
420 |
535 |
Parse = "~" ++ NumChars ++ "c", |
421 |
535 |
format_arg2(Arg, Parse); |
422 |
|
format_arg(Arg, binary) -> |
423 |
486 |
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 |
568 |
{ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), |
434 |
568 |
Arg2. |
435 |
|
|
436 |
|
%%----------------------------- |
437 |
|
%% Format result |
438 |
|
%%----------------------------- |
439 |
|
|
440 |
|
format_error(Error) -> |
441 |
9 |
try |
442 |
9 |
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 |
9 |
{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 |
41 |
io_lib:format("~s", [Binary]); |
460 |
|
format_result(Code, {_Name, rescode}) -> |
461 |
26 |
{"", make_status(Code)}; |
462 |
|
format_result({Code, Text}, {_Name, restuple}) -> |
463 |
118 |
{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 |
129 |
make_status(ok) -> ?STATUS_SUCCESS; |
493 |
:-( |
make_status(true) -> ?STATUS_SUCCESS; |
494 |
24 |
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 |
|
|