Skip to content

Commit 6bd8292

Browse files
author
Peter Tihanyi
committed
Add register command callback mode
1 parent 4c44183 commit 6bd8292

9 files changed

+197
-26
lines changed

README.md

+36-2
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,46 @@ cli_console:register(["list", "partitions"], [All, Node, Limit], fun list_partit
7575

7676
Or
7777

78-
Example:
7978
```erlang
8079
cli_console:register(["list", "partitions", Node], [All, Limit], funlist_partitions/1, "List partitions").
8180
```
8281

83-
####Run command
82+
This ad-hoc register command method does not survive if the cli_console service
83+
has restarted due to crash or anything else. Use register command callbacks
84+
instead.
85+
86+
#### Register command callbacks
87+
88+
There is `cli_console:register/2` function where the module and function parameters
89+
must be given.
90+
91+
First there mush define a function which contains the desired commands.
92+
It should return a list with tuples.
93+
Tuple should look like this (same as `cli_console:register/4` arguments):
94+
`{Commands :: [command()], Arguments:: [command_argument()], command_fun(), Description ::string()}`
95+
96+
Example module for callback:
97+
```erlang
98+
-module(test_module).
99+
100+
%% API
101+
-export([register_cli/0]).
102+
103+
-spec register_cli() -> [].
104+
register_cli() ->
105+
[
106+
{["clustre", "status"], [], fun(_) -> {text, "status: ok"} end, "Listpartitions" }
107+
].
108+
````
109+
110+
Registering callback:
111+
```erlang
112+
ok = cli_console:register(test_module, register_cli).
113+
```
114+
115+
Cluster status command will survive if cli_console process crashes or restarted.
116+
117+
#### Run command
84118

85119
Example:
86120
```erlang

rebar.config

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616

1717
{alias,[{test,[eunit,
1818
{ct,"--name ct --sys_config=config/sys.config --readable true --cover --verbose true"},
19-
{proper, "-n 1000"},
19+
{proper, "-n 2500"},
2020
dialyzer]}]}.

src/cli_console.erl

+10-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
-include("cli_console_command.hrl").
1212

1313
%% API
14-
-export([run/1, register/4]).
14+
-export([run/1, register/4, register/2]).
1515

1616
%% @doc Execute commands
1717
-spec run(string() | list(string())) -> ok.
@@ -21,11 +21,18 @@ run(ConsoleCommand) ->
2121
cli_console_output:output(Result),
2222
maybe_print_help(Result, Command).
2323

24-
%% @doc Register command
25-
-spec register([command()], [command_argument()], command_fun(), string()) -> ok.
24+
%% @doc Register ad-hoc command
25+
-spec register([command()], [command_argument()], command_fun(), string()) ->
26+
ok | {error, {multiple_argument_definitions, Name :: string()}}.
2627
register(Command, Arguments, Fun, Description) ->
2728
cli_console_command:register(Command, Arguments, Fun, Description).
2829

30+
%% @doc Register command register callback
31+
-spec register(module(), atom()) ->
32+
ok | {error, {multiple_argument_definitions, Name :: string()}}.
33+
register(Module, Function) ->
34+
cli_console_command:register(Module, Function).
35+
2936
maybe_print_help({error, command_not_found}, Command) ->
3037
cli_console_output:output(cli_console_command:get_help(Command));
3138
maybe_print_help(_, _) ->

src/cli_console_command.erl

+72-16
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
-include("cli_console_command.hrl").
1313
-include_lib("stdlib/include/ms_transform.hrl").
1414

15-
-export([start_link/0, register/4, run/2, get_help/1, handle_info/2]).
16-
-export([init/1, handle_call/3, handle_cast/2]).
15+
-export([start_link/0, register/4, register/2, run/2, get_help/1]).
16+
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
1717

1818
-define(SERVER, ?MODULE).
1919
-define(ETS_TABLE_NAME, cli_commands).
20+
-define(PERSISTENT_TERM_KEY, {?MODULE, register_command_callbacks}).
2021

2122
-record(state, {
2223
running_commands = [] :: [{CommandPid :: pid(), From :: {pid(), term()}}]
@@ -28,10 +29,16 @@
2829
%%% API
2930
%%%===================================================================
3031

31-
-spec register([command()], [command_argument()], command_fun(), string()) -> ok.
32+
-spec register([command()], [command_argument()], command_fun(), string()) ->
33+
ok | {error, {multiple_argument_definitions, Name :: string()}}.
3234
register(Command, Arguments, Fun, Description) ->
3335
gen_server:call(?SERVER, {register, Command, Arguments, Fun, Description}).
3436

37+
-spec register(module(), atom()) ->
38+
ok | {error, {multiple_argument_definitions, Name :: string()}}.
39+
register(Module, Function) ->
40+
gen_server:call(?SERVER, {register, Module, Function}).
41+
3542
-spec run([string()], proplists:proplist()) ->
3643
{ok, term()} |
3744
{error, command_not_found} |
@@ -58,7 +65,8 @@ start_link() ->
5865
-spec init(term()) -> {ok, state()}.
5966
init(_) ->
6067
process_flag(trap_exit, true),
61-
ets:new(cli_commands, [set, protected, named_table]),
68+
ets:new(?ETS_TABLE_NAME, [set, protected, named_table]),
69+
init_commands_from_registered_callback(),
6270
{ok, #state{}}.
6371

6472
-spec handle_call(term(), {pid(), Tag :: term()}, state()) ->
@@ -81,7 +89,15 @@ handle_call({get_help, Command}, _From, State = #state{}) ->
8189
{reply, {ok, [cli_console_formatter:title("Help~n") | Output]}, State};
8290
handle_call({register, Command, ArgsDef, Fun, Description}, _From,
8391
State = #state{}) ->
84-
{reply, register_command(Command, ArgsDef, Fun, Description), State}.
92+
{reply, register_command(Command, ArgsDef, Fun, Description), State};
93+
handle_call({register, Module, Function}, _From, State = #state{}) ->
94+
case catch register_commands_via_callback([{Module, Function}]) of
95+
ok ->
96+
store_command_register_callback(Module, Function),
97+
{reply, ok, State};
98+
Else ->
99+
{reply, {error, Else}, State}
100+
end.
85101

86102
-spec handle_cast(term(), state()) -> {noreply, state()}.
87103
handle_cast(_Request, State = #state{}) ->
@@ -123,9 +139,7 @@ run_command(Command, Args, ArgsDef, Fun) ->
123139
true ->
124140
get_help(Command);
125141
_ ->
126-
MissingArguments = get_missing_arguments(ArgsDef, NewArgs),
127-
evaluate_argument_check(MissingArguments,
128-
fun() -> execute_fun(Fun, NewArgs) end)
142+
evaluate_argument_check(ArgsDef, NewArgs, Fun)
129143
end;
130144
Else ->
131145
Else
@@ -141,6 +155,10 @@ execute_fun(Fun, NewArgs) ->
141155
]}
142156
end.
143157

158+
evaluate_argument_check(ArgsDef, NewArgs, Fun) ->
159+
MissingArguments = get_missing_arguments(ArgsDef, NewArgs),
160+
evaluate_argument_check(MissingArguments, fun() -> execute_fun(Fun, NewArgs) end).
161+
144162
evaluate_argument_check([], Fun) ->
145163
Fun();
146164
evaluate_argument_check(MissingArguments, _Fun) ->
@@ -281,7 +299,7 @@ format_command({Commands, Desc}) ->
281299
cli_console_formatter:text("~ts ~ts", [CommandStr, Desc]).
282300

283301
format_command_string(#argument{name = Name}) ->
284-
"<" ++ Name ++">";
302+
"<" ++ Name ++ ">";
285303
format_command_string(Command) ->
286304
Command.
287305

@@ -321,8 +339,10 @@ register_command(Command, ArgsDef, Fun, Description) ->
321339
CommandArguments = get_command_arguments(Command),
322340
case check_multiple_argdefs(ArgsDef ++ CommandArguments) of
323341
ok ->
324-
ets:match_delete(?ETS_TABLE_NAME, {generate_match_head(Command),
325-
generate_filter(Command), []}),
342+
% need to clean up previous commands to make does not register multiple
343+
% wildcard argument command
344+
[ets:delete(?ETS_TABLE_NAME, PrevCommand) ||
345+
{PrevCommand, _, _, _} <- get_wildcard_command(Command)],
326346
true = ets:insert(?ETS_TABLE_NAME, {Command, ArgsDef, Fun, Description}),
327347
ok;
328348
Else ->
@@ -388,17 +408,53 @@ generate_filter([], _ItemCount, Where) ->
388408
lists:reverse(Where);
389409
generate_filter([Command | Rest], ItemCount, Where) ->
390410
Item = item_num(ItemCount),
391-
Filter = {'orelse',
392-
{'==', Command, Item},
393-
{'==', argument, {element, 1, Item}}
394-
},
411+
Filter = generate_filter(Command, Item),
395412
generate_filter(Rest, ItemCount+1, [Filter | Where]).
396413

414+
generate_filter(#argument{}, Item) ->
415+
{'==', argument, {element, 1, Item}};
416+
generate_filter(Command, Item) ->
417+
{'orelse',
418+
{'==', Command, Item},
419+
{'==', argument, {element, 1, Item}}
420+
}.
421+
397422
item_num(ItemNum) ->
398423
list_to_atom("$" ++ integer_to_list(ItemNum)).
399424

400425
extract_command_args(CommandSpec, Command, Args) ->
401426
CommandArgWithIndex = get_command_arguments_with_index(CommandSpec),
402427
lists:foldl(fun({Index, #argument{name = Name}}, Acc) ->
403428
[{Name, lists:nth(Index, Command)} | Acc]
404-
end, Args, CommandArgWithIndex).
429+
end, Args, CommandArgWithIndex).
430+
431+
store_command_register_callback(Module, Function) ->
432+
Callbacks = get_command_register_callbacks(),
433+
Data = {Module, Function},
434+
NewCallbacks =
435+
case lists:member(Data, Callbacks) of
436+
true ->
437+
Callbacks;
438+
false ->
439+
[Data | Callbacks]
440+
end,
441+
persistent_term:put(?PERSISTENT_TERM_KEY, NewCallbacks).
442+
443+
get_command_register_callbacks() ->
444+
persistent_term:get(?PERSISTENT_TERM_KEY, []).
445+
446+
init_commands_from_registered_callback() ->
447+
register_commands_via_callback(get_command_register_callbacks()).
448+
449+
register_commands_via_callback([]) ->
450+
ok;
451+
register_commands_via_callback([{Module, Function} | Rest]) ->
452+
Result = apply(Module, Function, []),
453+
register_command_callback_result(Result),
454+
register_commands_via_callback(Rest).
455+
456+
register_command_callback_result([]) ->
457+
ok;
458+
register_command_callback_result([{Command, ArgDef, Fun, Description} | Rest]) ->
459+
ok = register_command(Command, ArgDef, Fun, Description),
460+
register_command_callback_result(Rest).

src/cli_console_command.hrl

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222

2323
-type command() :: string() | command_argument().
2424

25-
-export_type([command_argument/0]).
25+
-export_type([command_argument/0, command/0]).

src/cli_console_sup.erl

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ start_link() ->
2525
[ChildSpec :: supervisor:child_spec()]}}.
2626
init([]) ->
2727
SupFlags = #{strategy => one_for_all,
28-
intensity => 1,
29-
period => 10},
28+
intensity => 3,
29+
period => 100},
3030
ChildSpecs = [
3131
#{id => cli_command_server,
3232
start => {cli_console_command, start_link, []},

test/cli_console_SUITE.erl

+43-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ all() ->
120120
crash_in_function,
121121
default_value_flag,
122122
pid_not_in_state,
123-
wild_card_argument_help
123+
wild_card_argument_help,
124+
register_callback_ok,
125+
register_callback_empty,
126+
register_callback_bad_return,
127+
register_callback_restart
124128
].
125129

126130
%%--------------------------------------------------------------------
@@ -384,6 +388,44 @@ wild_card_argument_help(_Config) ->
384388
?assertEqual("Command not found\n\n\e[37;1mHelp\n\n\e[0mwild <table> <type> Wild help\n",
385389
catch_output(fun() -> cli_console:run(["wild"]) end)).
386390

391+
register_callback_ok(_Config) ->
392+
Fun = fun(_Args) -> [{text, "ok"}] end,
393+
Arg1 = cli_console_command_arg:argument("arg1", string, "Arg1 desc"),
394+
395+
ok = meck:new(cli_test_callback, [non_strict, passthrough]),
396+
meck:expect(cli_test_callback, register_cli,
397+
fun() -> [{["test", "cb"], [Arg1], Fun, "Test cb help"}] end),
398+
399+
?assertEqual(ok, cli_console:register(cli_test_callback, register_cli)),
400+
?assertEqual("ok",
401+
catch_output(fun() -> cli_console:run("test cb --arg1=ok") end)).
402+
403+
register_callback_empty(_Config) ->
404+
ok = meck:new(cli_test_callback, [non_strict, passthrough]),
405+
meck:expect(cli_test_callback, register_cli, fun() -> [] end),
406+
407+
?assertEqual(ok, cli_console:register(cli_test_callback, register_cli)).
408+
409+
register_callback_bad_return(_Config) ->
410+
ok = meck:new(cli_test_callback, [non_strict, passthrough]),
411+
meck:expect(cli_test_callback, register_cli, fun() -> foo end),
412+
413+
?assertMatch({error, _}, cli_console:register(cli_test_callback, register_cli)).
414+
415+
register_callback_restart(Config) ->
416+
register_callback_ok(Config),
417+
418+
% restart command sever
419+
[exit(Pid, kill) ||
420+
{Id, Pid, worker, _} <- supervisor:which_children(cli_console_sup),
421+
Id =:= cli_command_server],
422+
423+
% wait a litle to supervisor restart the died process
424+
ct:sleep(200),
425+
?assertEqual("ok",
426+
catch_output(fun() -> cli_console:run("test cb --arg1=ok") end)).
427+
428+
387429
list_partitions(Args) ->
388430
[cli_console_formatter:title("List of partions"),
389431
cli_console_formatter:separator(),

test/cli_console_command_SUITE.erl

+16
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ all() ->
110110
register_wildcard,
111111
register_multiple_wildcard,
112112
register_wildcard_same_arg,
113+
register_wildcard_different_args,
113114
command_not_found,
114115
mandatory_arg_not_set_found,
115116
mandatory_arg_not_set_but_has_default,
@@ -165,6 +166,21 @@ register_wildcard_same_arg(_Config) ->
165166
[WildcardArgument], Fun, "Wild help"),
166167
?assertEqual({error, {multiple_argument_definitions, "wildcard"}}, Result).
167168

169+
register_wildcard_different_args(_Config) ->
170+
WildcardArgument = cli_console_command_arg:argument("wildcard", string,
171+
"Desc of wildcard"),
172+
WildcardArgumentDefault =
173+
cli_console_command_arg:set_default(WildcardArgument, "test"),
174+
ok = cli_console_command:register(["wild", WildcardArgument], [],
175+
fun(_Args) -> {text, "1"} end,
176+
"Wild help"),
177+
178+
ok = cli_console_command:register(["wild", WildcardArgumentDefault], [],
179+
fun(_Args) -> {text, "2"} end,
180+
"Wild help"),
181+
?assertEqual({ok, {text, "2"}},
182+
cli_console_command:run(["wild", "things"], [])).
183+
168184
command_not_found(_Config) ->
169185
?assertEqual({error, command_not_found},
170186
cli_console_command:run(["not_found_command"], [])).

test/cli_test_callback.erl

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
%%%-------------------------------------------------------------------
2+
%%% @author Peter Tihanyi
3+
%%% @copyright (C) 2021, Systream
4+
%%% @doc
5+
%%%
6+
%%% @end
7+
%%%-------------------------------------------------------------------
8+
-module(cli_test_callback).
9+
-author("Peter Tihanyi").
10+
11+
%% API
12+
-export([register_cli/0]).
13+
14+
-spec register_cli() -> [].
15+
register_cli() ->
16+
[].

0 commit comments

Comments
 (0)