1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_ctl.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : ejabberd command line admin tool |
5 |
|
%%% Created : 11 Jan 2004 by Alexey Shchepin <alexey@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
%%% @headerfile "ejabberd_ctl.hrl" |
27 |
|
|
28 |
|
%%% @doc Management of mongooseimctl commands and frontend to ejabberd commands. |
29 |
|
%%% |
30 |
|
%%% An mongooseimctl command is an abstract function identified by a |
31 |
|
%%% name, with a defined number of calling arguments, that can be |
32 |
|
%%% defined in any Erlang module and executed using mongooseimctl |
33 |
|
%%% administration script. |
34 |
|
%%% |
35 |
|
%%% Note: strings cannot have blankspaces |
36 |
|
%%% |
37 |
|
%%% Does not support commands that have arguments with ctypes: list, tuple |
38 |
|
%%% |
39 |
|
%%% TODO: Update the guide |
40 |
|
%%% TODO: Mention this in the release notes |
41 |
|
%%% Note: the commands with several words use now the underline: _ |
42 |
|
%%% It is still possible to call the commands with dash: - |
43 |
|
%%% but this is deprecated, and may be removed in a future version. |
44 |
|
|
45 |
|
|
46 |
|
-module(ejabberd_ctl). |
47 |
|
-author('alexey@process-one.net'). |
48 |
|
|
49 |
|
-export([start/0, |
50 |
|
init/0, |
51 |
|
process/1, |
52 |
|
process2/2, |
53 |
|
register_commands/3, |
54 |
|
unregister_commands/3]). |
55 |
|
|
56 |
|
-ignore_xref([process/1, process2/2, register_commands/3, start/0, unregister_commands/3]). |
57 |
|
|
58 |
|
-include("ejabberd_ctl.hrl"). |
59 |
|
-include("ejabberd_commands.hrl"). |
60 |
|
-include("mongoose.hrl"). |
61 |
|
|
62 |
|
-type format() :: integer | string | binary | {list, format()}. |
63 |
|
-type format_type() :: binary() | string() | char(). |
64 |
|
-type cmd() :: {CallString :: string(), Args :: [string()], Desc :: string()}. |
65 |
|
|
66 |
|
-define(ASCII_SPACE_CHARACTER, $\s). |
67 |
|
-define(PRINT(Format, Args), io:format(lists:flatten(Format), Args)). |
68 |
|
-define(TIME_HMS_FORMAT, "~B days ~2.10.0B:~2.10.0B:~2.10.0B"). |
69 |
|
-define(a2l(A), atom_to_list(A)). |
70 |
|
|
71 |
|
%%----------------------------- |
72 |
|
%% Module |
73 |
|
%%----------------------------- |
74 |
|
|
75 |
|
-spec start() -> none(). |
76 |
|
start() -> |
77 |
:-( |
case init:get_plain_arguments() of |
78 |
|
[SNode | Args0] -> |
79 |
:-( |
Args = args_join_xml(args_join_strings(Args0)), |
80 |
:-( |
SNode1 = case string:tokens(SNode, "@") of |
81 |
|
[_Node, _Server] -> |
82 |
:-( |
SNode; |
83 |
|
_ -> |
84 |
:-( |
case net_kernel:longnames() of |
85 |
|
true -> |
86 |
:-( |
SNode ++ "@" ++ inet_db:gethostname() ++ |
87 |
|
"." ++ inet_db:res_option(domain); |
88 |
|
false -> |
89 |
:-( |
SNode ++ "@" ++ inet_db:gethostname(); |
90 |
|
_ -> |
91 |
:-( |
SNode |
92 |
|
end |
93 |
|
end, |
94 |
:-( |
Node = list_to_atom(SNode1), |
95 |
:-( |
Status = case rpc:call(Node, ?MODULE, process, [Args]) of |
96 |
|
{badrpc, Reason} -> |
97 |
:-( |
?PRINT("Failed RPC connection to the node ~p: ~p~n", |
98 |
|
[Node, Reason]), |
99 |
|
%% TODO: show minimal start help |
100 |
:-( |
?STATUS_BADRPC; |
101 |
|
S -> |
102 |
:-( |
S |
103 |
|
end, |
104 |
:-( |
halt(Status); |
105 |
|
_ -> |
106 |
:-( |
print_usage(), |
107 |
:-( |
halt(?STATUS_USAGE) |
108 |
|
end. |
109 |
|
|
110 |
|
|
111 |
|
-spec init() -> atom() | ets:tid(). |
112 |
|
init() -> |
113 |
83 |
ets:new(ejabberd_ctl_cmds, [named_table, set, public]), |
114 |
83 |
ets:new(ejabberd_ctl_host_cmds, [named_table, set, public]). |
115 |
|
|
116 |
|
|
117 |
|
%%----------------------------- |
118 |
|
%% mongooseimctl Command managment |
119 |
|
%%----------------------------- |
120 |
|
|
121 |
|
-spec register_commands(CmdDescs :: [tuple()] | tuple(), |
122 |
|
Module :: atom(), |
123 |
|
Function :: atom()) -> 'ok'. |
124 |
|
register_commands(CmdDescs, Module, Function) -> |
125 |
:-( |
ets:insert(ejabberd_ctl_cmds, CmdDescs), |
126 |
:-( |
ejabberd_hooks:add(ejabberd_ctl_process, global, Module, Function, 50), |
127 |
:-( |
ok. |
128 |
|
|
129 |
|
|
130 |
|
-spec unregister_commands(CmdDescs :: [any()], |
131 |
|
Module :: atom(), |
132 |
|
Function :: atom()) -> 'ok'. |
133 |
|
unregister_commands(CmdDescs, Module, Function) -> |
134 |
:-( |
lists:foreach(fun(CmdDesc) -> |
135 |
:-( |
ets:delete_object(ejabberd_ctl_cmds, CmdDesc) |
136 |
|
end, CmdDescs), |
137 |
:-( |
ejabberd_hooks:delete(ejabberd_ctl_process, global, Module, Function, 50), |
138 |
:-( |
ok. |
139 |
|
|
140 |
|
|
141 |
|
%%----------------------------- |
142 |
|
%% Process |
143 |
|
%%----------------------------- |
144 |
|
|
145 |
|
%% @doc The commands status, stop and restart are defined here to ensure |
146 |
|
%% they are usable even if ejabberd is completely stopped. |
147 |
|
-spec process(_) -> integer(). |
148 |
|
process(["status"]) -> |
149 |
:-( |
{InternalStatus, ProvidedStatus} = init:get_status(), |
150 |
:-( |
MongooseStatus = get_mongoose_status(), |
151 |
:-( |
?PRINT("~s", [format_status([{node, node()}, {internal_status, InternalStatus}, |
152 |
|
{provided_status, ProvidedStatus}, |
153 |
|
{mongoose_status, MongooseStatus}, |
154 |
|
{os_pid, os:getpid()}, get_uptime(), |
155 |
|
{dist_proto, get_dist_proto()}, |
156 |
|
{logs, mongoose_logs:get_log_files()}])]), |
157 |
:-( |
case MongooseStatus of |
158 |
:-( |
not_running -> ?STATUS_ERROR; |
159 |
:-( |
{running, _, _Version} -> ?STATUS_SUCCESS |
160 |
|
end; |
161 |
|
process(["stop"]) -> |
162 |
|
%%ejabberd_cover:stop(), |
163 |
:-( |
init:stop(), |
164 |
:-( |
?STATUS_SUCCESS; |
165 |
|
process(["restart"]) -> |
166 |
:-( |
init:restart(), |
167 |
:-( |
?STATUS_SUCCESS; |
168 |
|
process(["mnesia"]) -> |
169 |
:-( |
?PRINT("~p~n", [mnesia:system_info(all)]), |
170 |
:-( |
?STATUS_SUCCESS; |
171 |
|
process(["mnesia", "info"]) -> |
172 |
:-( |
mnesia:info(), |
173 |
:-( |
?STATUS_SUCCESS; |
174 |
|
process(["mnesia", Arg]) when is_list(Arg) -> |
175 |
:-( |
case catch mnesia:system_info(list_to_atom(Arg)) of |
176 |
:-( |
{'EXIT', Error} -> ?PRINT("Error: ~p~n", [Error]); |
177 |
:-( |
Return -> ?PRINT("~p~n", [Return]) |
178 |
|
end, |
179 |
:-( |
?STATUS_SUCCESS; |
180 |
|
process(["graphql", Arg]) when is_list(Arg) -> |
181 |
2 |
Doc = list_to_binary(Arg), |
182 |
2 |
Ep = mongoose_graphql:get_endpoint(admin), |
183 |
2 |
case mongoose_graphql:execute(Ep, undefined, Doc) of |
184 |
|
{ok, Result} -> |
185 |
1 |
PrettyResult = jiffy:encode(Result, [pretty]), |
186 |
1 |
?PRINT("~s\n", [PrettyResult]); |
187 |
|
{error, _} = Err -> |
188 |
1 |
?PRINT("~p\n", [Err]) |
189 |
|
end, |
190 |
2 |
?STATUS_SUCCESS; |
191 |
|
process(["graphql" | _]) -> |
192 |
2 |
?PRINT("This command requires one string type argument!\n", []), |
193 |
2 |
?STATUS_ERROR; |
194 |
|
|
195 |
|
%% @doc The arguments --long and --dual are not documented because they are |
196 |
|
%% automatically selected depending in the number of columns of the shell |
197 |
|
process(["help" | Mode]) -> |
198 |
:-( |
{MaxC, ShCode} = get_shell_info(), |
199 |
:-( |
case Mode of |
200 |
|
[] -> |
201 |
:-( |
print_usage(dual, MaxC, ShCode), |
202 |
:-( |
?STATUS_USAGE; |
203 |
|
["--dual"] -> |
204 |
:-( |
print_usage(dual, MaxC, ShCode), |
205 |
:-( |
?STATUS_USAGE; |
206 |
|
["--long"] -> |
207 |
:-( |
print_usage(long, MaxC, ShCode), |
208 |
:-( |
?STATUS_USAGE; |
209 |
|
["--tags"] -> |
210 |
:-( |
print_usage_tags(MaxC, ShCode), |
211 |
:-( |
?STATUS_SUCCESS; |
212 |
|
["--tags", Tag] -> |
213 |
:-( |
print_usage_tags(Tag, MaxC, ShCode), |
214 |
:-( |
?STATUS_SUCCESS; |
215 |
|
["help"] -> |
216 |
:-( |
print_usage_help(MaxC, ShCode), |
217 |
:-( |
?STATUS_SUCCESS; |
218 |
|
[CmdString | _] -> |
219 |
:-( |
CmdStringU = re:replace(CmdString, "-", "_", [global, {return, list}]), |
220 |
:-( |
print_usage_commands(CmdStringU, MaxC, ShCode), |
221 |
:-( |
?STATUS_SUCCESS |
222 |
|
end; |
223 |
|
process(Args) -> |
224 |
226 |
AccessCommands = get_accesscommands(), |
225 |
226 |
{String, Code} = process2(Args, AccessCommands), |
226 |
226 |
case String of |
227 |
83 |
[] -> ok; |
228 |
|
_ -> |
229 |
143 |
io:format("~s~n", [String]) |
230 |
|
end, |
231 |
226 |
Code. |
232 |
|
|
233 |
|
|
234 |
|
-spec process2(Args :: [string()], AccessCommands :: ejabberd_commands:access_commands()) -> |
235 |
|
{String::string(), Code::integer()}. |
236 |
|
process2(["--auth", User, Server, Pass | Args], AccessCommands) -> |
237 |
:-( |
process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass)}, |
238 |
|
AccessCommands); |
239 |
|
process2(Args, AccessCommands) -> |
240 |
226 |
process2(Args, noauth, AccessCommands). |
241 |
|
|
242 |
|
|
243 |
|
%% @private |
244 |
|
process2(Args, Auth, AccessCommands) -> |
245 |
226 |
case try_run_ctp(Args, Auth, AccessCommands) of |
246 |
|
{String, wrong_command_arguments} when is_list(String) -> |
247 |
:-( |
io:format(lists:flatten(["\n" | String]++["\n"])), |
248 |
:-( |
[CommandString | _] = Args, |
249 |
:-( |
process(["help" | [CommandString]]), |
250 |
:-( |
{lists:flatten(String), ?STATUS_ERROR}; |
251 |
|
{String, Code} when is_list(String) and is_integer(Code) -> |
252 |
192 |
{lists:flatten(String), Code}; |
253 |
|
String when is_list(String) -> |
254 |
34 |
{lists:flatten(String), ?STATUS_SUCCESS}; |
255 |
|
Other -> |
256 |
:-( |
{"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR} |
257 |
|
end. |
258 |
|
|
259 |
|
|
260 |
|
-spec get_accesscommands() -> ejabberd_commands:access_commands(). |
261 |
|
get_accesscommands() -> |
262 |
226 |
mongoose_config:get_opt(mongooseimctl_access_commands). |
263 |
|
|
264 |
|
|
265 |
|
%%----------------------------- |
266 |
|
%% Command calling |
267 |
|
%%----------------------------- |
268 |
|
|
269 |
|
-spec try_run_ctp(Args :: [string()], |
270 |
|
Auth :: ejabberd_commands:auth(), |
271 |
|
AccessCommands :: ejabberd_commands:access_commands() |
272 |
|
) -> string() | integer() | {string(), integer()} | {string(), wrong_command_arguments}. |
273 |
|
try_run_ctp(Args, Auth, AccessCommands) -> |
274 |
226 |
try mongoose_hooks:ejabberd_ctl_process(false, Args) of |
275 |
|
false when Args /= [] -> |
276 |
226 |
try_call_command(Args, Auth, AccessCommands); |
277 |
|
false -> |
278 |
:-( |
print_usage(), |
279 |
:-( |
{"", ?STATUS_USAGE}; |
280 |
|
Status -> |
281 |
:-( |
{"", Status} |
282 |
|
catch |
283 |
|
exit:Why -> |
284 |
:-( |
print_usage(), |
285 |
:-( |
{io_lib:format("Error in mongooseimctl process: ~p", [Why]), ?STATUS_USAGE}; |
286 |
|
Error:Why -> |
287 |
|
%% In this case probably ejabberd is not started, so let's show Status |
288 |
:-( |
process(["status"]), |
289 |
:-( |
?PRINT("~n", []), |
290 |
:-( |
{io_lib:format("Error in mongooseimctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} |
291 |
|
end. |
292 |
|
|
293 |
|
|
294 |
|
-spec try_call_command(Args :: [string()], |
295 |
|
Auth :: ejabberd_commands:auth(), |
296 |
|
AccessCommands :: ejabberd_commands:access_commands() |
297 |
|
) -> string() | integer() | {string(), integer()} | {string(), wrong_command_arguments}. |
298 |
|
try_call_command(Args, Auth, AccessCommands) -> |
299 |
226 |
try call_command(Args, Auth, AccessCommands) of |
300 |
|
{error, command_unknown} -> |
301 |
:-( |
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR}; |
302 |
|
Res -> |
303 |
226 |
Res |
304 |
|
catch |
305 |
|
A:Why:Stack -> |
306 |
:-( |
{io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR} |
307 |
|
end. |
308 |
|
|
309 |
|
|
310 |
|
-spec call_command(Args :: [string()], |
311 |
|
Auth :: ejabberd_commands:auth(), |
312 |
|
AccessCommands :: ejabberd_commands:access_commands() |
313 |
|
) -> string() | integer() | {string(), integer()} |
314 |
|
| {string(), wrong_command_arguments} | {error, command_unknown}. |
315 |
|
call_command([CmdString | Args], Auth, AccessCommands) -> |
316 |
226 |
CmdStringU = re:replace(CmdString, "-", "_", [global, {return, list}]), |
317 |
226 |
Command = list_to_atom(CmdStringU), |
318 |
226 |
case ejabberd_commands:get_command_format(Command) of |
319 |
|
{error, command_unknown} -> |
320 |
:-( |
{error, command_unknown}; |
321 |
|
{ArgsFormat, ResultFormat} -> |
322 |
226 |
case (catch format_args(Args, ArgsFormat)) of |
323 |
|
ArgsFormatted when is_list(ArgsFormatted) -> |
324 |
226 |
Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, |
325 |
|
ArgsFormatted), |
326 |
226 |
format_result(Result, ResultFormat); |
327 |
|
{'EXIT', {function_clause, [{lists, zip, [A1, A2], _FileInfo} | _]}} -> |
328 |
:-( |
{NumCompa, TextCompa} = |
329 |
|
case {length(A1), length(A2)} of |
330 |
:-( |
{L1, L2} when L1 < L2 -> {L2-L1, "less argument"}; |
331 |
:-( |
{L1, L2} when L1 > L2 -> {L1-L2, "more argument"} |
332 |
|
end, |
333 |
:-( |
{io_lib:format("Error: the command ~p requires ~p ~s.", |
334 |
|
[CmdString, NumCompa, TextCompa]), |
335 |
|
wrong_command_arguments} |
336 |
|
end |
337 |
|
end. |
338 |
|
|
339 |
|
|
340 |
|
%%----------------------------- |
341 |
|
%% Format arguments |
342 |
|
%%----------------------------- |
343 |
|
|
344 |
|
%% @private |
345 |
|
-spec args_join_xml([string()]) -> [string()]. |
346 |
|
args_join_xml([]) -> |
347 |
:-( |
[]; |
348 |
|
args_join_xml([ [ $< | _ ] = Arg | RArgs ]) -> |
349 |
:-( |
case bal(Arg, $<, $>) of |
350 |
|
true -> |
351 |
:-( |
[Arg | args_join_xml(RArgs)]; |
352 |
|
false -> |
353 |
:-( |
[NextArg | RArgs1] = RArgs, |
354 |
:-( |
args_join_xml([Arg ++ " " ++ NextArg | RArgs1]) |
355 |
|
end; |
356 |
|
args_join_xml([ Arg | RArgs ]) -> |
357 |
:-( |
[ Arg | args_join_xml(RArgs) ]. |
358 |
|
|
359 |
|
|
360 |
|
%% @private |
361 |
|
-spec args_join_strings([string()]) -> [string()]. |
362 |
|
args_join_strings([]) -> |
363 |
:-( |
[]; |
364 |
|
args_join_strings([ "\"", NextArg | RArgs ]) -> |
365 |
:-( |
args_join_strings([ "\"" ++ NextArg | RArgs ]); |
366 |
|
args_join_strings([ [ $" | _ ] = Arg | RArgs ]) -> |
367 |
:-( |
case lists:nthtail(length(Arg)-2, Arg) of |
368 |
|
[C1, $"] when C1 /= ?ASCII_SPACE_CHARACTER -> |
369 |
:-( |
[ string:substr(Arg, 2, length(Arg)-2) | args_join_strings(RArgs) ]; |
370 |
|
_ -> |
371 |
:-( |
[NextArg | RArgs1] = RArgs, |
372 |
:-( |
args_join_strings([Arg ++ " " ++ NextArg | RArgs1]) |
373 |
|
end; |
374 |
|
args_join_strings([ Arg | RArgs ]) -> |
375 |
:-( |
[ Arg | args_join_strings(RArgs) ]. |
376 |
|
|
377 |
|
|
378 |
|
%% @private |
379 |
|
-spec bal(string(), char(), char()) -> boolean(). |
380 |
|
bal(String, Left, Right) -> |
381 |
:-( |
bal(String, Left, Right, 0). |
382 |
|
|
383 |
|
|
384 |
|
%% @private |
385 |
|
-spec bal(string(), L :: char(), R :: char(), Bal :: integer()) -> boolean(). |
386 |
|
bal([], _Left, _Right, Bal) -> |
387 |
:-( |
Bal == 0; |
388 |
|
bal([?ASCII_SPACE_CHARACTER, _NextChar | T], Left, Right, Bal) -> |
389 |
:-( |
bal(T, Left, Right, Bal); |
390 |
|
bal([Left | T], Left, Right, Bal) -> |
391 |
:-( |
bal(T, Left, Right, Bal-1); |
392 |
|
bal([Right | T], Left, Right, Bal) -> |
393 |
:-( |
bal(T, Left, Right, Bal+1); |
394 |
|
bal([_C | T], Left, Right, Bal) -> |
395 |
:-( |
bal(T, Left, Right, Bal). |
396 |
|
|
397 |
|
|
398 |
|
%% @private |
399 |
|
-spec format_args(Args :: [any()], |
400 |
|
ArgsFormat :: [format()]) -> [any()]. |
401 |
|
format_args(Args, ArgsFormat) -> |
402 |
226 |
lists:foldl( |
403 |
|
fun({{_ArgName, ArgFormat}, Arg}, Res) -> |
404 |
679 |
Formatted = format_arg(Arg, ArgFormat), |
405 |
679 |
Res ++ [Formatted] |
406 |
|
end, |
407 |
|
[], |
408 |
|
lists:zip(ArgsFormat, Args)). |
409 |
|
|
410 |
|
|
411 |
|
%% @private |
412 |
|
-spec format_arg(string(), format()) -> format_type(). |
413 |
|
format_arg(Arg, integer) -> |
414 |
25 |
format_arg2(Arg, "~d"); |
415 |
|
format_arg("", string) -> |
416 |
6 |
""; |
417 |
|
format_arg(Arg, string) -> |
418 |
649 |
NumChars = integer_to_list(string:len(Arg)), |
419 |
649 |
Parse = "~" ++ NumChars ++ "c", |
420 |
649 |
format_arg2(Arg, Parse); |
421 |
|
format_arg(Arg, binary) -> |
422 |
598 |
list_to_binary(format_arg(Arg, string)); |
423 |
|
format_arg(Arg, {list, Type}) -> |
424 |
1 |
[format_arg(Token, Type) || Token <- string:tokens(Arg, ";")]. |
425 |
|
|
426 |
|
|
427 |
|
%% @private |
428 |
|
-spec format_arg2(Arg :: string(), |
429 |
|
Parse :: nonempty_string() |
430 |
|
) -> [[any()] | char()] | char(). |
431 |
|
format_arg2(Arg, Parse)-> |
432 |
674 |
{ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), |
433 |
674 |
Arg2. |
434 |
|
|
435 |
|
%%----------------------------- |
436 |
|
%% Format result |
437 |
|
%%----------------------------- |
438 |
|
|
439 |
|
format_error(Error) -> |
440 |
9 |
try |
441 |
9 |
io_lib:format("\"~ts\"", [Error]) |
442 |
|
catch _ -> |
443 |
:-( |
io_lib:format("~p", [Error]) |
444 |
|
end. |
445 |
|
|
446 |
|
-spec format_result(In :: tuple() | atom() | integer() | string() | binary(), |
447 |
|
{_, 'atom'|'integer'|'string'|'binary'} |
448 |
|
) -> string() | {string(), _}. |
449 |
|
format_result({error, Error}, _) -> |
450 |
9 |
{io_lib:format("Error: ~ts", [format_error(Error)]), make_status(error)}; |
451 |
|
format_result(Atom, {_Name, atom}) -> |
452 |
:-( |
io_lib:format("~p", [Atom]); |
453 |
|
format_result(Int, {_Name, integer}) -> |
454 |
37 |
io_lib:format("~p", [Int]); |
455 |
|
format_result(String, {_Name, string}) -> |
456 |
45 |
io_lib:format("~s", [String]); |
457 |
|
format_result(Binary, {_Name, binary}) -> |
458 |
40 |
io_lib:format("~s", [Binary]); |
459 |
|
format_result(Code, {_Name, rescode}) -> |
460 |
65 |
{"", make_status(Code)}; |
461 |
|
format_result({Code, Text}, {_Name, restuple}) -> |
462 |
118 |
{io_lib:format("~s", [Text]), make_status(Code)}; |
463 |
|
%% The result is a list of something: [something()] |
464 |
|
format_result([], {_Name, {list, _ElementsDef}}) -> |
465 |
2 |
""; |
466 |
|
format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> |
467 |
|
%% Start formatting the first element |
468 |
11 |
[format_result(FirstElement, ElementsDef) | |
469 |
|
%% If there are more elements, put always first a newline character |
470 |
|
lists:map( |
471 |
|
fun(Element) -> |
472 |
20 |
["\n" | format_result(Element, ElementsDef)] |
473 |
|
end, |
474 |
|
Elements)]; |
475 |
|
%% The result is a tuple with several elements: {something1(), something2(), ...} |
476 |
|
%% NOTE: the elements in the tuple are separated with tabular characters, |
477 |
|
%% if a string is empty, it will be difficult to notice in the shell, |
478 |
|
%% maybe a different separation character should be used, like ;;? |
479 |
|
format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) -> |
480 |
14 |
ElementsList = tuple_to_list(ElementsTuple), |
481 |
14 |
[{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef), |
482 |
14 |
[format_result(FirstE, FirstD) | |
483 |
|
lists:map( |
484 |
|
fun({Element, ElementDef}) -> |
485 |
70 |
["\t" | format_result(Element, ElementDef)] |
486 |
|
end, |
487 |
|
ElementsAndDef)]. |
488 |
|
|
489 |
|
|
490 |
|
-spec make_status(ok | true | _) -> 0 | 1. |
491 |
168 |
make_status(ok) -> ?STATUS_SUCCESS; |
492 |
:-( |
make_status(true) -> ?STATUS_SUCCESS; |
493 |
24 |
make_status(_Error) -> ?STATUS_ERROR. |
494 |
|
|
495 |
|
|
496 |
|
-spec get_list_commands() |
497 |
|
-> [{Call :: string(), Args :: [string()], Desc :: string()}]. |
498 |
|
get_list_commands() -> |
499 |
:-( |
try ejabberd_commands:list_commands() of |
500 |
|
Commands -> |
501 |
:-( |
[tuple_command_help(Command) |
502 |
:-( |
|| {N, _, _}=Command <- Commands, |
503 |
|
%% Don't show again those commands, because they are already |
504 |
|
%% announced by ejabberd_ctl itself |
505 |
:-( |
N /= status, N /= stop, N /= restart] |
506 |
|
catch |
507 |
|
exit:_ -> |
508 |
:-( |
[] |
509 |
|
end. |
510 |
|
|
511 |
|
|
512 |
|
-spec tuple_command_help(ejabberd_commands:list_cmd()) -> cmd(). |
513 |
|
tuple_command_help({Name, Args, Desc}) -> |
514 |
:-( |
Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args], |
515 |
:-( |
CallString = atom_to_list(Name), |
516 |
:-( |
{CallString, Arguments, Desc}. |
517 |
|
|
518 |
|
|
519 |
|
-spec get_list_ctls() -> [cmd()]. |
520 |
|
get_list_ctls() -> |
521 |
:-( |
case catch ets:tab2list(ejabberd_ctl_cmds) of |
522 |
:-( |
{'EXIT', _} -> []; |
523 |
:-( |
Cs -> [{NameArgs, [], Desc} || {NameArgs, Desc} <- Cs] |
524 |
|
end. |
525 |
|
|
526 |
|
format_status([{node, Node}, {internal_status, IS}, {provided_status, PS}, |
527 |
|
{mongoose_status, MS}, {os_pid, OSPid}, {uptime, UptimeHMS}, |
528 |
|
{dist_proto, DistProto}, {logs, LogFiles}]) -> |
529 |
|
( ["MongooseIM node ", ?a2l(Node), ":\n", |
530 |
|
" operating system pid: ", OSPid, "\n", |
531 |
|
" Erlang VM status: ", ?a2l(IS), " (of: starting | started | stopping)\n", |
532 |
|
" boot script status: ", io_lib:format("~p", [PS]), "\n", |
533 |
|
" version: ", case MS of |
534 |
:-( |
{running, App, Version} -> [Version, " (as ", ?a2l(App), ")"]; |
535 |
:-( |
not_running -> "unavailable - neither ejabberd nor mongooseim is running" |
536 |
|
end, "\n", |
537 |
|
" uptime: ", io_lib:format(?TIME_HMS_FORMAT, UptimeHMS), "\n", |
538 |
:-( |
" distribution protocol: ", DistProto, "\n"] ++ |
539 |
:-( |
[" logs: none - maybe enable logging to a file in app.config?\n" || LogFiles == [] ] ++ |
540 |
:-( |
[" logs:\n" || LogFiles /= [] ] ++ [ |
541 |
:-( |
[" ", LogFile, "\n"] || LogFile <- LogFiles ] ). |
542 |
|
|
543 |
|
%%----------------------------- |
544 |
|
%% Print help |
545 |
|
%%----------------------------- |
546 |
|
|
547 |
|
%% Bold |
548 |
|
-define(B1, "\e[1m"). |
549 |
|
-define(B2, "\e[22m"). |
550 |
|
-define(B(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end). |
551 |
|
|
552 |
|
%% Underline |
553 |
|
-define(U1, "\e[4m"). |
554 |
|
-define(U2, "\e[24m"). |
555 |
|
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). |
556 |
|
|
557 |
|
print_usage() -> |
558 |
:-( |
{MaxC, ShCode} = get_shell_info(), |
559 |
:-( |
print_usage(dual, MaxC, ShCode). |
560 |
|
|
561 |
|
|
562 |
|
-spec print_usage('dual' | 'long' |
563 |
|
, MaxC :: integer() |
564 |
|
, ShCode :: boolean()) -> 'ok'. |
565 |
|
print_usage(HelpMode, MaxC, ShCode) -> |
566 |
:-( |
AllCommands = |
567 |
|
[ |
568 |
|
{"status", [], "Get MongooseIM status"}, |
569 |
|
{"stop", [], "Stop MongooseIM"}, |
570 |
|
{"restart", [], "Restart MongooseIM"}, |
571 |
|
{"help", ["[--tags [tag] | com?*]"], "Show help (try: mongooseimctl help help)"}, |
572 |
|
{"mnesia", ["[info]"], "show information of Mnesia system"}, |
573 |
|
{"graphql", ["query"], "Execute graphql query or mutation"}] ++ |
574 |
|
get_list_commands() ++ |
575 |
|
get_list_ctls(), |
576 |
|
|
577 |
:-( |
?PRINT( |
578 |
:-( |
["Usage: ", ?B("mongooseimctl"), " [--node ", ?U("nodename"), "] [--auth ", |
579 |
:-( |
?U("user"), " ", ?U("host"), " ", ?U("password"), "] ", |
580 |
:-( |
?U("command"), " [", ?U("options"), "]\n" |
581 |
|
"\n" |
582 |
|
"Available commands in this MongooseIM node:\n"], []), |
583 |
:-( |
print_usage_commands(HelpMode, MaxC, ShCode, AllCommands), |
584 |
:-( |
?PRINT( |
585 |
|
["\n" |
586 |
|
"Examples:\n" |
587 |
|
" mongooseimctl restart\n" |
588 |
|
" mongooseimctl --node mongooseim@host restart\n"], |
589 |
|
[]). |
590 |
|
|
591 |
|
|
592 |
|
-spec print_usage_commands(HelpMode :: 'dual' | 'long', |
593 |
|
MaxC :: integer(), |
594 |
|
ShCode :: boolean(), |
595 |
|
Commands :: [cmd(), ...]) -> 'ok'. |
596 |
|
print_usage_commands(HelpMode, MaxC, ShCode, Commands) -> |
597 |
:-( |
CmdDescsSorted = lists:keysort(1, Commands), |
598 |
|
|
599 |
|
%% What is the length of the largest command? |
600 |
:-( |
{CmdArgsLenDescsSorted, Lens} = |
601 |
|
lists:mapfoldl( |
602 |
|
fun({Cmd, Args, Desc}, Lengths) -> |
603 |
:-( |
Len = |
604 |
|
length(Cmd) + |
605 |
|
lists:foldl(fun(Arg, R) -> |
606 |
:-( |
R + 1 + length(Arg) |
607 |
|
end, |
608 |
|
0, |
609 |
|
Args), |
610 |
:-( |
{{Cmd, Args, Len, Desc}, [Len | Lengths]} |
611 |
|
end, |
612 |
|
[], |
613 |
|
CmdDescsSorted), |
614 |
:-( |
MaxCmdLen = case Lens of |
615 |
:-( |
[] -> 80; |
616 |
:-( |
_ -> lists:max(Lens) |
617 |
|
end, |
618 |
|
|
619 |
|
%% For each command in the list of commands |
620 |
|
%% Convert its definition to a line |
621 |
:-( |
FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode, HelpMode), |
622 |
|
|
623 |
:-( |
?PRINT([FmtCmdDescs], []). |
624 |
|
|
625 |
|
|
626 |
|
%% @doc Get some info about the shell: how many columns of width and guess if |
627 |
|
%% it supports text formatting codes. |
628 |
|
-spec get_shell_info() -> {integer(), boolean()}. |
629 |
|
get_shell_info() -> |
630 |
:-( |
case io:columns() of |
631 |
:-( |
{ok, C} -> {C-2, true}; |
632 |
:-( |
{error, enotsup} -> {78, false} |
633 |
|
end. |
634 |
|
|
635 |
|
|
636 |
|
%% @doc Split this command description in several lines of proper length |
637 |
|
-spec prepare_description(DescInit :: non_neg_integer(), |
638 |
|
MaxC :: integer(), |
639 |
|
Desc :: string()) -> [[[any()]], ...]. |
640 |
|
prepare_description(DescInit, MaxC, Desc) -> |
641 |
:-( |
Words = string:tokens(Desc, " "), |
642 |
:-( |
prepare_long_line(DescInit, MaxC, Words). |
643 |
|
|
644 |
|
|
645 |
|
-spec prepare_long_line(DescInit :: non_neg_integer(), |
646 |
|
MaxC :: integer(), |
647 |
|
Words :: [nonempty_string()] |
648 |
|
) -> [[[any()]], ...]. |
649 |
|
prepare_long_line(DescInit, MaxC, Words) -> |
650 |
:-( |
MaxSegmentLen = MaxC - DescInit, |
651 |
:-( |
MarginString = lists:duplicate(DescInit, ?ASCII_SPACE_CHARACTER), % Put spaces |
652 |
:-( |
[FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words), |
653 |
:-( |
MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments), |
654 |
:-( |
[FirstSegment | MoreSegmentsMixed]. |
655 |
|
|
656 |
|
|
657 |
|
-spec mix_desc_segments(MarginStr :: [any()], |
658 |
|
Segments :: [[[any(), ...]]]) -> [[[any()], ...]]. |
659 |
|
mix_desc_segments(MarginString, Segments) -> |
660 |
:-( |
[["\n", MarginString, Segment] || Segment <- Segments]. |
661 |
|
|
662 |
|
split_desc_segments(MaxL, Words) -> |
663 |
:-( |
join(MaxL, Words). |
664 |
|
|
665 |
|
|
666 |
|
%% @doc Join words in a segment, but stop adding to a segment if adding this |
667 |
|
%% word would pass L |
668 |
|
-spec join(L :: number(), Words :: [nonempty_string()]) -> [[[any(), ...]], ...]. |
669 |
|
join(L, Words) -> |
670 |
:-( |
join(L, Words, 0, [], []). |
671 |
|
|
672 |
|
|
673 |
|
-spec join(L :: number(), |
674 |
|
Words :: [nonempty_string()], |
675 |
|
LenLastSeg :: non_neg_integer(), |
676 |
|
LastSeg :: [nonempty_string()], |
677 |
|
ResSeg :: [[[any(), ...]]] ) -> [[[any(), ...]], ...]. |
678 |
|
join(_L, [], _LenLastSeg, LastSeg, ResSeg) -> |
679 |
:-( |
ResSeg2 = [lists:reverse(LastSeg) | ResSeg], |
680 |
:-( |
lists:reverse(ResSeg2); |
681 |
|
join(L, [Word | Words], LenLastSeg, LastSeg, ResSeg) -> |
682 |
:-( |
LWord = length(Word), |
683 |
:-( |
case LWord + LenLastSeg < L of |
684 |
|
true -> |
685 |
|
%% This word fits in the last segment |
686 |
|
%% If this word ends with "\n", reset column counter |
687 |
:-( |
case string:str(Word, "\n") of |
688 |
|
0 -> |
689 |
:-( |
join(L, Words, LenLastSeg+LWord+1, [" ", Word | LastSeg], ResSeg); |
690 |
|
_ -> |
691 |
:-( |
join(L, Words, LWord+1, [" ", Word | LastSeg], ResSeg) |
692 |
|
end; |
693 |
|
false -> |
694 |
:-( |
join(L, Words, LWord, [" ", Word], [lists:reverse(LastSeg) | ResSeg]) |
695 |
|
end. |
696 |
|
|
697 |
|
|
698 |
|
-spec format_command_lines(CALD :: [{[any()], [any()], number(), _}, ...], |
699 |
|
MaxCmdLen :: integer(), |
700 |
|
MaxC :: integer(), |
701 |
|
ShCode :: boolean(), |
702 |
|
'dual' | 'long') -> [[any(), ...], ...]. |
703 |
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) |
704 |
|
when MaxC - MaxCmdLen < 40 -> |
705 |
|
%% If the space available for descriptions is too narrow, enforce long help mode |
706 |
:-( |
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long); |
707 |
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) -> |
708 |
:-( |
lists:map( |
709 |
|
fun({Cmd, Args, CmdArgsL, Desc}) -> |
710 |
:-( |
DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc), |
711 |
:-( |
[" ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], string:chars(?ASCII_SPACE_CHARACTER, MaxCmdLen - CmdArgsL + 1), |
712 |
|
DescFmt, "\n"] |
713 |
|
end, CALD); |
714 |
|
format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> |
715 |
:-( |
lists:map( |
716 |
|
fun({Cmd, Args, _CmdArgsL, Desc}) -> |
717 |
:-( |
DescFmt = prepare_description(8, MaxC, Desc), |
718 |
:-( |
["\n ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], "\n", " ", |
719 |
|
DescFmt, "\n"] |
720 |
|
end, CALD). |
721 |
|
|
722 |
|
|
723 |
|
%%----------------------------- |
724 |
|
%% Print Tags |
725 |
|
%%----------------------------- |
726 |
|
|
727 |
|
print_usage_tags(MaxC, ShCode) -> |
728 |
:-( |
?PRINT("Available tags and commands:", []), |
729 |
:-( |
TagsCommands = ejabberd_commands:get_tags_commands(), |
730 |
:-( |
lists:foreach( |
731 |
|
fun({Tag, Commands} = _TagCommands) -> |
732 |
:-( |
?PRINT(["\n\n ", ?B(Tag), "\n "], []), |
733 |
:-( |
Words = lists:sort(Commands), |
734 |
:-( |
Desc = prepare_long_line(5, MaxC, Words), |
735 |
:-( |
?PRINT(Desc, []) |
736 |
|
end, |
737 |
|
TagsCommands), |
738 |
:-( |
?PRINT("\n\n", []). |
739 |
|
|
740 |
|
|
741 |
|
print_usage_tags(Tag, MaxC, ShCode) -> |
742 |
:-( |
?PRINT(["Available commands with tag ", ?B(Tag), ":", "\n"], []), |
743 |
:-( |
HelpMode = long, |
744 |
:-( |
TagsCommands = ejabberd_commands:get_tags_commands(), |
745 |
:-( |
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of |
746 |
:-( |
{value, {Tag, CNs}} -> CNs; |
747 |
:-( |
false -> [] |
748 |
|
end, |
749 |
:-( |
CommandsList = lists:map( |
750 |
|
fun(NameString) -> |
751 |
:-( |
C = ejabberd_commands:get_command_definition(list_to_atom(NameString)), |
752 |
:-( |
#ejabberd_commands{name = Name, |
753 |
|
args = Args, |
754 |
|
desc = Desc} = C, |
755 |
:-( |
tuple_command_help({Name, Args, Desc}) |
756 |
|
end, |
757 |
|
CommandsNames), |
758 |
:-( |
print_usage_commands(HelpMode, MaxC, ShCode, CommandsList), |
759 |
:-( |
?PRINT("\n", []). |
760 |
|
|
761 |
|
|
762 |
|
%%----------------------------- |
763 |
|
%% Print usage of 'help' command |
764 |
|
%%----------------------------- |
765 |
|
|
766 |
|
print_usage_help(MaxC, ShCode) -> |
767 |
:-( |
LongDesc = |
768 |
|
["The special 'help' mongooseimctl command provides help of MongooseIM commands.\n\n" |
769 |
:-( |
"The format is:\n ", ?B("mongooseimctl"), " ", ?B("help"), " [", ?B("--tags"), " ", ?U("[tag]"), " | ", ?U("com?*"), "]\n\n" |
770 |
|
"The optional arguments:\n" |
771 |
:-( |
" ", ?B("--tags"), " Show all tags and the names of commands in each tag\n" |
772 |
:-( |
" ", ?B("--tags"), " ", ?U("tag"), " Show description of commands in this tag\n" |
773 |
:-( |
" ", ?U("command"), " Show detailed description of the command\n" |
774 |
:-( |
" ", ?U("com?*"), " Show detailed description of commands that match this glob.\n" |
775 |
|
" You can use ? to match a simple character, \n" |
776 |
|
" and * to match several characters.\n" |
777 |
|
"\n", |
778 |
|
"Some example usages:\n", |
779 |
|
" mongooseimctl help\n", |
780 |
|
" mongooseimctl help --tags\n", |
781 |
|
" mongooseimctl help --tags accounts\n", |
782 |
|
" mongooseimctl help register\n", |
783 |
|
" mongooseimctl help regist*\n", |
784 |
|
"\n", |
785 |
|
"Please note that 'mongooseimctl help' shows all MongooseIM commands, \n", |
786 |
|
"even those that cannot be used in the shell with mongooseimctl.\n", |
787 |
|
"Those commands can be identified because the description starts with: *"], |
788 |
:-( |
ArgsDef = [], |
789 |
:-( |
C = #ejabberd_commands{ |
790 |
|
name = help, |
791 |
|
desc = "Show help of MongooseIM commands", |
792 |
|
longdesc = lists:flatten(LongDesc), |
793 |
|
args = ArgsDef, |
794 |
|
module = none, |
795 |
|
function = none, |
796 |
|
result = {help, string}}, |
797 |
:-( |
print_usage_command("help", C, MaxC, ShCode). |
798 |
|
|
799 |
|
|
800 |
|
%%----------------------------- |
801 |
|
%% Print usage command |
802 |
|
%%----------------------------- |
803 |
|
|
804 |
|
-spec print_usage_commands(CmdSubString :: string(), MaxC :: integer(), ShCode :: boolean()) -> ok. |
805 |
|
print_usage_commands(CmdSubString, MaxC, ShCode) -> |
806 |
|
%% Get which command names match this substring |
807 |
:-( |
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()], |
808 |
:-( |
Cmds = filter_commands(AllCommandsNames, CmdSubString), |
809 |
:-( |
case Cmds of |
810 |
:-( |
[] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]); |
811 |
:-( |
_ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode) |
812 |
|
end. |
813 |
|
|
814 |
|
|
815 |
|
print_usage_commands2(Cmds, MaxC, ShCode) -> |
816 |
|
%% Then for each one print it |
817 |
:-( |
lists:mapfoldl( |
818 |
|
fun(Cmd, Remaining) -> |
819 |
:-( |
print_usage_command(Cmd, MaxC, ShCode), |
820 |
:-( |
case Remaining > 1 of |
821 |
:-( |
true -> ?PRINT([" ", lists:duplicate(MaxC, 126), " \n"], []); |
822 |
:-( |
false -> ok |
823 |
|
end, |
824 |
:-( |
{ok, Remaining-1} |
825 |
|
end, |
826 |
|
length(Cmds), |
827 |
|
Cmds). |
828 |
|
|
829 |
|
|
830 |
|
filter_commands(All, SubString) -> |
831 |
:-( |
case lists:member(SubString, All) of |
832 |
:-( |
true -> [SubString]; |
833 |
:-( |
false -> filter_commands_regexp(All, SubString) |
834 |
|
end. |
835 |
|
|
836 |
|
|
837 |
|
filter_commands_regexp(All, Glob) -> |
838 |
:-( |
RegExp = xmerl_regexp:sh_to_awk(Glob), |
839 |
:-( |
lists:filter( |
840 |
|
fun(Command) -> |
841 |
:-( |
case re:run(Command, RegExp, [{capture, none}]) of |
842 |
|
match -> |
843 |
:-( |
true; |
844 |
|
nomatch -> |
845 |
:-( |
false |
846 |
|
end |
847 |
|
end, |
848 |
|
All). |
849 |
|
|
850 |
|
|
851 |
|
-spec print_usage_command(Cmd :: string(), MaxC :: integer(), ShCode :: boolean()) -> ok. |
852 |
|
print_usage_command(Cmd, MaxC, ShCode) -> |
853 |
:-( |
Name = list_to_atom(Cmd), |
854 |
:-( |
case ejabberd_commands:get_command_definition(Name) of |
855 |
|
command_not_found -> |
856 |
:-( |
io:format("Error: command ~p not known.~n", [Cmd]); |
857 |
|
C -> |
858 |
:-( |
print_usage_command(Cmd, C, MaxC, ShCode) |
859 |
|
end. |
860 |
|
|
861 |
|
|
862 |
|
print_usage_command(Cmd, C, MaxC, ShCode) -> |
863 |
:-( |
#ejabberd_commands{ |
864 |
|
tags = TagsAtoms, |
865 |
|
desc = Desc, |
866 |
|
longdesc = LongDesc, |
867 |
|
args = ArgsDef, |
868 |
|
result = ResultDef} = C, |
869 |
|
|
870 |
:-( |
NameFmt = [" ", ?B("Command Name"), ": ", Cmd, "\n"], |
871 |
|
|
872 |
|
%% Initial indentation of result is 13 = length(" Arguments: ") |
873 |
:-( |
Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef], |
874 |
:-( |
ArgsMargin = lists:duplicate(13, ?ASCII_SPACE_CHARACTER), |
875 |
:-( |
ArgsListFmt = case Args of |
876 |
:-( |
[] -> "\n"; |
877 |
:-( |
_ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args] |
878 |
|
end, |
879 |
:-( |
ArgsFmt = [" ", ?B("Arguments"), ": ", ArgsListFmt], |
880 |
|
|
881 |
|
%% Initial indentation of result is 11 = length(" Returns: ") |
882 |
:-( |
ResultFmt = format_usage_ctype(ResultDef, 11), |
883 |
:-( |
ReturnsFmt = [" ", ?B("Returns"), ": ", ResultFmt], |
884 |
|
|
885 |
:-( |
TagsFmt = [" ", ?B("Tags"), ": ", prepare_long_line(8, MaxC, [atom_to_list(TagA) || TagA <- TagsAtoms])], |
886 |
|
|
887 |
:-( |
DescFmt = [" ", ?B("Description"), ": ", prepare_description(15, MaxC, Desc)], |
888 |
|
|
889 |
:-( |
LongDescFmt = case LongDesc of |
890 |
:-( |
"" -> ""; |
891 |
:-( |
_ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"] |
892 |
|
end, |
893 |
|
|
894 |
:-( |
?PRINT(["\n", NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, "\n\n", TagsFmt, "\n\n", DescFmt, "\n\n", LongDescFmt], []). |
895 |
|
|
896 |
|
|
897 |
|
format_usage_ctype(Type, _Indentation) |
898 |
|
when (Type==atom) or (Type==integer) or (Type==string) or (Type==rescode) or (Type==restuple) or (Type==binary)-> |
899 |
:-( |
io_lib:format("~p", [Type]); |
900 |
|
format_usage_ctype({Name, Type}, _Indentation) |
901 |
|
when (Type==atom) or (Type==integer) or (Type==string) or (Type==rescode) or (Type==restuple) or (Type==binary)-> |
902 |
:-( |
io_lib:format("~p::~p", [Name, Type]); |
903 |
|
format_usage_ctype({Name, {list, ElementDef}}, Indentation) -> |
904 |
:-( |
NameFmt = atom_to_list(Name), |
905 |
:-( |
Indentation2 = Indentation + length(NameFmt) + 4, |
906 |
:-( |
ElementFmt = format_usage_ctype(ElementDef, Indentation2), |
907 |
:-( |
[NameFmt, "::[ ", ElementFmt, " ]"]; |
908 |
|
format_usage_ctype({Name, {tuple, ElementsDef}}, Indentation) -> |
909 |
:-( |
NameFmt = atom_to_list(Name), |
910 |
:-( |
Indentation2 = Indentation + length(NameFmt) + 4, |
911 |
:-( |
ElementsFmt = format_usage_tuple(ElementsDef, Indentation2), |
912 |
:-( |
[NameFmt, "::{ " | ElementsFmt]. |
913 |
|
|
914 |
|
|
915 |
|
format_usage_tuple([], _Indentation) -> |
916 |
:-( |
[]; |
917 |
|
format_usage_tuple([ElementDef], Indentation) -> |
918 |
:-( |
[format_usage_ctype(ElementDef, Indentation), " }"]; |
919 |
|
format_usage_tuple([ElementDef | ElementsDef], Indentation) -> |
920 |
:-( |
ElementFmt = format_usage_ctype(ElementDef, Indentation), |
921 |
:-( |
MarginString = lists:duplicate(Indentation, ?ASCII_SPACE_CHARACTER), % Put spaces |
922 |
:-( |
[ElementFmt, ", \n", MarginString, format_usage_tuple(ElementsDef, Indentation)]. |
923 |
|
|
924 |
|
get_mongoose_status() -> |
925 |
:-( |
case lists:keyfind(mongooseim, 1, application:which_applications()) of |
926 |
|
false -> |
927 |
:-( |
not_running; |
928 |
|
{_, _, Version} -> |
929 |
:-( |
{running, mongooseim, Version} |
930 |
|
end. |
931 |
|
|
932 |
|
get_uptime() -> |
933 |
:-( |
{MilliSeconds, _} = erlang:statistics(wall_clock), |
934 |
:-( |
{D, {H, M, S}} = calendar:seconds_to_daystime(MilliSeconds div 1000), |
935 |
:-( |
{uptime, [D, H, M, S]}. |
936 |
|
|
937 |
|
get_dist_proto() -> |
938 |
|
%% See kernel/src/net_kernel.erl |
939 |
:-( |
case init:get_argument(proto_dist) of |
940 |
:-( |
{ok, [Proto]} -> Proto; |
941 |
:-( |
_ -> "inet_tcp" |
942 |
|
end. |
943 |
|
|