From c119b418432f8742da17fab2084c5ef6ea0df51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 13 Nov 2020 16:15:34 +0100 Subject: [PATCH 01/51] New prototype with commands defined by broker, not cli --- deps/rabbit/Makefile | 18 +++ deps/rabbit/src/rabbit_cli.erl | 180 ++++++++++++++++++++++++ deps/rabbit/src/rabbit_cli_commands.erl | 74 ++++++++++ deps/rabbit/src/rabbit_cli_io.erl | 150 ++++++++++++++++++++ deps/rabbit/src/sysexits.hrl | 17 +++ 5 files changed, 439 insertions(+) create mode 100644 deps/rabbit/src/rabbit_cli.erl create mode 100644 deps/rabbit/src/rabbit_cli_commands.erl create mode 100644 deps/rabbit/src/rabbit_cli_io.erl create mode 100644 deps/rabbit/src/sysexits.hrl diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index 5153918dd56f..7a6d502c00f2 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -158,6 +158,24 @@ DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk include ../../rabbitmq-components.mk include ../../erlang.mk +ESCRIPT_NAME := rabbit_cli +ESCRIPT_FILE := scripts/rmq + +ebin/$(PROJECT).app:: $(ESCRIPT_FILE) + +$(ESCRIPT_FILE): ebin/rabbit_cli.beam + $(gen_verbose) printf "%s\n" \ + "#!$(ESCRIPT_SHEBANG)" \ + "%% $(ESCRIPT_COMMENT)" \ + "%%! $(ESCRIPT_EMU_ARGS)" > $(ESCRIPT_FILE) + $(verbose) cat $< >> $(ESCRIPT_FILE) + $(verbose) chmod a+x $(ESCRIPT_FILE) + +clean:: clean-cli + +clean-cli: + $(gen_verbose) rm -f $(ESCRIPT_FILE) + ifeq ($(strip $(BATS)),) BATS := $(ERLANG_MK_TMP)/bats/bin/bats endif diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl new file mode 100644 index 000000000000..0b306a40ab11 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli.erl @@ -0,0 +1,180 @@ +-module(rabbit_cli). + +-include_lib("kernel/include/logger.hrl"). + +-export([main/1]). + +main(Args) -> + Ret = run_cli(Args), + io:format(standard_error, "Ret: ~p~n", [Ret]), + erlang:halt(). + +run_cli(Args) -> + maybe + Progname = escript:script_name(), + add_rabbitmq_code_path(Progname), + + PartialArgparseDef = argparse_def(), + {ok, + PartialArgMap, + PartialCmdPath, + PartialCommand} ?= initial_parse(Progname, Args, PartialArgparseDef), + + %% Get remote node name and prepare Erlang distribution. + Nodename = lookup_rabbitmq_nodename(PartialArgMap), + {ok, _} ?= net_kernel:start( + undefined, #{name_domain => shortnames}), + + {ok, IO} ?= rabbit_cli_io:start_link(Progname), + try + %% Can we reach the remote node? + case net_kernel:connect_node(Nodename) of + true -> + maybe + %% We can query the argparse definition from the + %% remote node to know the commands it supports and + %% proceed with the execution. + ArgparseDef = get_final_argparse_def(Nodename), + {ok, + ArgMap, + CmdPath, + Command} ?= final_parse(Progname, Args, ArgparseDef), + run_command( + Nodename, ArgparseDef, + Progname, ArgMap, CmdPath, Command, + IO) + end; + false -> + %% We can't reach the remote node. Let's fallback + %% to a local execution. + run_command( + undefined, PartialArgparseDef, + Progname, PartialArgMap, PartialCmdPath, + PartialCommand, IO) + end + after + rabbit_cli_io:stop(IO) + end + end. + +add_rabbitmq_code_path(Progname) -> + ScriptDir = filename:dirname(Progname), + PluginsDir0 = filename:join([ScriptDir, "..", "plugins"]), + PluginsDir1 = case filelib:is_dir(PluginsDir0) of + true -> + PluginsDir0 + end, + Glob = filename:join([PluginsDir1, "*", "ebin"]), + AppDirs = filelib:wildcard(Glob), + lists:foreach(fun code:add_path/1, AppDirs), + ok. + +argparse_def() -> + #{arguments => + [ + #{name => help, + long => "-help", + short => $h, + type => boolean, + help => "Display help and exit"}, + #{name => node, + long => "-node", + short => $n, + type => string, + nargs => 1, + help => "Name of the node to control"}, + #{name => verbose, + long => "-verbose", + short => $v, + action => count, + help => + "Be verbose; can be specified multiple times to increase verbosity"}, + #{name => version, + long => "-version", + short => $V, + help => + "Display version and exit"} + ], + + commands => #{}}. + +initial_parse(Progname, Args, ArgparseDef) -> + Options = #{progname => Progname}, + case partial_parse(Args, ArgparseDef, Options) of + {ok, ArgMap, CmdPath, Command, _RemainingArgs} -> + {ok, ArgMap, CmdPath, Command}; + {error, _} = Error-> + Error + end. + +partial_parse(Args, ArgparseDef, Options) -> + partial_parse(Args, ArgparseDef, Options, []). + +partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> + case argparse:parse(Args, ArgparseDef, Options) of + {ok, ArgMap, CmdPath, Command} -> + RemainingArgs1 = lists:reverse(RemainingArgs), + {ok, ArgMap, CmdPath, Command, RemainingArgs1}; + {error, {_CmdPath, undefined, Arg, <<>>}} -> + Args1 = Args -- [Arg], + RemainingArgs1 = [Arg | RemainingArgs], + partial_parse(Args1, ArgparseDef, Options, RemainingArgs1); + {error, _} = Error -> + Error + end. + +get_final_argparse_def(Nodename) -> + ArgparseDef1 = argparse_def(), + ArgparseDef2 = erpc:call(Nodename, rabbit_cli_commands, argparse_def, []), + ArgparseDef = maps:merge(ArgparseDef1, ArgparseDef2), + ArgparseDef. + +final_parse(Progname, Args, ArgparseDef) -> + Options = #{progname => Progname}, + argparse:parse(Args, ArgparseDef, Options). + +lookup_rabbitmq_nodename(#{node := Nodename}) -> + Nodename1 = complete_nodename(Nodename), + Nodename1; +lookup_rabbitmq_nodename(_) -> + GuessedNodename0 = guess_rabbitmq_nodename(), + GuessedNodename1 = complete_nodename(GuessedNodename0), + GuessedNodename1. + +guess_rabbitmq_nodename() -> + case net_adm:names() of + {ok, NamesAndPorts} -> + Names0 = [Name || {Name, _Port} <- NamesAndPorts], + Names1 = lists:sort(Names0), + Names2 = lists:filter( + fun + ("rabbit" ++ _) -> true; + (_) -> false + end, Names1), + case Names2 of + [First | _] -> + First; + [] -> + "rabbit" + end; + {error, address} -> + "rabbit" + end. + +complete_nodename(Nodename) -> + case re:run(Nodename, "@", [{capture, none}]) of + nomatch -> + {ok, ThisHost} = inet:gethostname(), + list_to_atom(Nodename ++ "@" ++ ThisHost); + match -> + list_to_atom(Nodename) + end. + +run_command( + _Nodename, ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> + rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); +run_command(Nodename, _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> + erpc:call( + Nodename, + rabbit_cli_commands, run_command, + [Progname, ArgMap, CmdPath, Command, IO]). diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl new file mode 100644 index 000000000000..6ef13d55cef1 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -0,0 +1,74 @@ +-module(rabbit_cli_commands). + +-include_lib("kernel/include/logger.hrl"). + +-export([argparse_def/0, run_command/5]). +-export([list_exchanges/5]). + +argparse_def() -> + %% Extract the commands from module attributes like feature flags and boot + %% steps. + #{commands => + #{"list" => + #{help => "List entities", + commands => + #{"exchanges" => + maps:merge( + rabbit_cli_io:argparse_def(record_stream), + #{help => "List exchanges", + handler => {?MODULE, list_exchanges}}) + } + } + } + }. + +run_command(Progname, ArgMap, CmdPath, Command, IO) -> + %% TODO: Put both processes under the rabbit supervision tree. + RunnerPid = command_runner(Progname, ArgMap, CmdPath, Command, IO), + RunnerMRef = erlang:monitor(process, RunnerPid), + receive + {'DOWN', RunnerMRef, _, _, Reason} -> + {ok, Reason} + end. + +command_runner( + Progname, ArgMap, CmdPath, #{handler := {Mod, Fun}} = Command, IO) -> + spawn_link(Mod, Fun, [Progname, ArgMap, CmdPath, Command, IO]). + +list_exchanges(_Progname, ArgMap, _CmdPath, _Command, IO) -> + InfoKeys = rabbit_exchange:info_keys(), + Fields = lists:map( + fun + (name = Key) -> + #{name => Key, type => resource}; + (type = Key) -> + #{name => Key, type => string}; + (durable = Key) -> + #{name => Key, type => boolean}; + (auto_delete = Key) -> + #{name => Key, type => boolean}; + (internal = Key) -> + #{name => Key, type => boolean}; + (arguments = Key) -> + #{name => Key, type => term}; + (policy = Key) -> + #{name => Key, type => string}; + (user_who_performed_action = Key) -> + #{name => Key, type => string}; + (Key) -> + #{name => Key, type => term} + end, InfoKeys), + case rabbit_cli_io:start_record_stream(IO, exchanges, Fields, ArgMap) of + {ok, Stream} -> + Exchanges = rabbit_exchange:list(), + lists:foreach( + fun(Exchange) -> + Record0 = rabbit_exchange:info(Exchange), + Record1 = lists:sublist(Record0, length(Fields)), + Record2 = [Value || {_Key, Value} <- Record1], + rabbit_cli_io:push_new_record(IO, Stream, Record2) + end, Exchanges), + rabbit_cli_io:end_record_stream(IO, Stream); + {error, _} = Error -> + Error + end. diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl new file mode 100644 index 000000000000..f95efec0d884 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -0,0 +1,150 @@ +-module(rabbit_cli_io). + +-include_lib("kernel/include/logger.hrl"). + +-include_lib("rabbit_common/include/resource.hrl"). + +-export([start_link/1, + stop/1, + argparse_def/1, + display_help/3, + start_record_stream/4, + push_new_record/3, + end_record_stream/2]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-record(?MODULE, {progname, + record_streams = #{}}). + +start_link(Progname) -> + gen_server:start_link(rabbit_cli_io, #{progname => Progname}, []). + +stop(IO) -> + MRef = erlang:monitor(process, IO), + _ = gen_server:call(IO, stop), + receive + {'DOWN', MRef, _, _, _Reason} -> + ok + end. + +argparse_def(record_stream) -> + #{arguments => + [ + #{name => output, + long => "-output", + short => $o, + type => string, + nargs => 1, + help => "Write output to file "}, + #{name => format, + long => "-format", + short => $f, + type => {atom, [plain, json]}, + nargs => 1, + help => "Format output acccording to "} + ] + }. + +display_help(IO, CmdPath, Command) -> + gen_server:cast(IO, {?FUNCTION_NAME, CmdPath, Command}). + +start_record_stream(IO, Name, Fields, ArgMap) + when is_pid(IO) andalso + is_atom(Name) andalso + is_map(ArgMap) -> + gen_server:call(IO, {?FUNCTION_NAME, Name, Fields, ArgMap}). + +push_new_record(IO, #{name := Name}, Record) -> + gen_server:cast(IO, {?FUNCTION_NAME, Name, Record}). + +end_record_stream(IO, #{name := Name}) -> + gen_server:cast(IO, {?FUNCTION_NAME, Name}). + +init(#{progname := Progname}) -> + process_flag(trap_exit, true), + State = #?MODULE{progname = Progname}, + {ok, State}. + +handle_call( + {start_record_stream, Name, Fields, _ArgMap}, + From, + #?MODULE{record_streams = Streams} = State) -> + Stream = #{name => Name, fields => Fields}, + gen_server:reply(From, {ok, Stream}), + + FieldNames = [atom_to_list(FieldName) + || #{name := FieldName} <- Fields], + Header = string:join(FieldNames, "\t"), + io:format("~ts~n", [Header]), + + Streams1 = Streams#{Name => Stream}, + State1 = State#?MODULE{record_streams = Streams1}, + {noreply, State1}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast( + {display_help, CmdPath, ArgparseDef}, + #?MODULE{progname = Progname} = State) -> + Options = #{progname => Progname, + %% Work around bug in argparse; + %% See https://github.com/erlang/otp/pull/9160 + command => tl(CmdPath)}, + Help = argparse:help(ArgparseDef, Options), + io:format("~s~n", [Help]), + {noreply, State}; +handle_cast( + {push_new_record, Name, Record}, + #?MODULE{record_streams = Streams} = State) -> + #{fields := Fields} = maps:get(Name, Streams), + Values = format_fields(Fields, Record), + Line = string:join(Values, "\t"), + io:format("~ts~n", [Line]), + {noreply, State}; +handle_cast({end_record_stream, _Name}, State) -> + {noreply, State}; +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +format_fields(Fields, Values) -> + format_fields(Fields, Values, []). + +format_fields([#{type := string} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~ts", [Value]), + Acc1 = [String | Acc], + format_fields(Rest1, Rest2, Acc1); +format_fields([#{type := integer} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~b", [Value]), + Acc1 = [String | Acc], + format_fields(Rest1, Rest2, Acc1); +format_fields([#{type := boolean} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~ts", [if Value -> "☑"; true -> "☐" end]), + Acc1 = [String | Acc], + format_fields(Rest1, Rest2, Acc1); +format_fields([#{type := resource} | Rest1], [Value | Rest2], Acc) -> + #resource{name = Name} = Value, + String = io_lib:format("~ts", [Name]), + Acc1 = [String | Acc], + format_fields(Rest1, Rest2, Acc1); +format_fields([#{type := term} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~0p", [Value]), + Acc1 = [String | Acc], + format_fields(Rest1, Rest2, Acc1); +format_fields([], [], Acc) -> + lists:reverse(Acc). diff --git a/deps/rabbit/src/sysexits.hrl b/deps/rabbit/src/sysexits.hrl new file mode 100644 index 000000000000..995b40bfbc3b --- /dev/null +++ b/deps/rabbit/src/sysexits.hrl @@ -0,0 +1,17 @@ +-define(EX_OK, 0). + +-define(EX_USAGE, 64). % Command line usage error +-define(EX_DATAERR, 65). % Data format error +-define(EX_NOINPUT, 66). % Cannot open input +-define(EX_NOUSER, 67). % Addressee unknown +-define(EX_NOHOST, 68). % Host name unknown +-define(EX_UNAVAILABLE, 69). % Service unavailable +-define(EX_SOFTWARE, 70). % Internal software error +-define(EX_OSERR, 71). % System error (e.g., can't fork) +-define(EX_OSFILE, 72). % Critical OS file missing +-define(EX_CANTCREAT, 73). % Can't create (user) output file +-define(EX_IOERR, 74). % Input/output error +-define(EX_TEMPFAIL, 75). % Temp failure; user is invited to retry +-define(EX_PROTOCOL, 76). % Remote error in protocol +-define(EX_NOPERM, 77). % Permission denied +-define(EX_CONFIG, 78). % Configuration error From a87a64ddf3bc6113c78d510886f131f452bfd01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 26 Dec 2024 20:01:18 +0100 Subject: [PATCH 02/51] Add transport layer --- deps/rabbit/src/rabbit_cli.erl | 130 +++++++++-------------- deps/rabbit/src/rabbit_cli_transport.erl | 90 ++++++++++++++++ 2 files changed, 142 insertions(+), 78 deletions(-) create mode 100644 deps/rabbit/src/rabbit_cli_transport.erl diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index 0b306a40ab11..e0c263a79a56 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -12,48 +12,49 @@ main(Args) -> run_cli(Args) -> maybe Progname = escript:script_name(), - add_rabbitmq_code_path(Progname), + ok ?= add_rabbitmq_code_path(Progname), + {ok, IO} ?= rabbit_cli_io:start_link(Progname), + + try + parse_command_pass1(Progname, Args, IO) + after + rabbit_cli_io:stop(IO) + end + end. + +parse_command_pass1(Progname, Args, IO) -> + maybe PartialArgparseDef = argparse_def(), {ok, PartialArgMap, PartialCmdPath, PartialCommand} ?= initial_parse(Progname, Args, PartialArgparseDef), - %% Get remote node name and prepare Erlang distribution. - Nodename = lookup_rabbitmq_nodename(PartialArgMap), - {ok, _} ?= net_kernel:start( - undefined, #{name_domain => shortnames}), - - {ok, IO} ?= rabbit_cli_io:start_link(Progname), - try - %% Can we reach the remote node? - case net_kernel:connect_node(Nodename) of - true -> - maybe - %% We can query the argparse definition from the - %% remote node to know the commands it supports and - %% proceed with the execution. - ArgparseDef = get_final_argparse_def(Nodename), - {ok, - ArgMap, - CmdPath, - Command} ?= final_parse(Progname, Args, ArgparseDef), - run_command( - Nodename, ArgparseDef, - Progname, ArgMap, CmdPath, Command, - IO) - end; - false -> - %% We can't reach the remote node. Let's fallback - %% to a local execution. - run_command( - undefined, PartialArgparseDef, - Progname, PartialArgMap, PartialCmdPath, - PartialCommand, IO) - end - after - rabbit_cli_io:stop(IO) + case rabbit_cli_transport:connect(PartialArgMap) of + {ok, Connection} -> + %% We can query the argparse definition from the remote node + %% to know the commands it supports and proceed with the + %% execution. + maybe + ArgparseDef = get_final_argparse_def(Connection), + {ok, + ArgMap, + CmdPath, + Command} ?= final_parse(Progname, Args, ArgparseDef), + + run_remote_command( + Connection, ArgparseDef, + Progname, ArgMap, CmdPath, Command, + IO) + end; + {error, _} -> + %% We can't reach the remote node. Let's fallback + %% to a local execution. + run_local_command( + PartialArgparseDef, + Progname, PartialArgMap, PartialCmdPath, + PartialCommand, IO) end end. @@ -123,9 +124,10 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> Error end. -get_final_argparse_def(Nodename) -> +get_final_argparse_def(Connection) -> ArgparseDef1 = argparse_def(), - ArgparseDef2 = erpc:call(Nodename, rabbit_cli_commands, argparse_def, []), + ArgparseDef2 = rabbit_cli_transport:rpc( + Connection, rabbit_cli_commands, argparse_def, []), ArgparseDef = maps:merge(ArgparseDef1, ArgparseDef2), ArgparseDef. @@ -133,48 +135,20 @@ final_parse(Progname, Args, ArgparseDef) -> Options = #{progname => Progname}, argparse:parse(Args, ArgparseDef, Options). -lookup_rabbitmq_nodename(#{node := Nodename}) -> - Nodename1 = complete_nodename(Nodename), - Nodename1; -lookup_rabbitmq_nodename(_) -> - GuessedNodename0 = guess_rabbitmq_nodename(), - GuessedNodename1 = complete_nodename(GuessedNodename0), - GuessedNodename1. - -guess_rabbitmq_nodename() -> - case net_adm:names() of - {ok, NamesAndPorts} -> - Names0 = [Name || {Name, _Port} <- NamesAndPorts], - Names1 = lists:sort(Names0), - Names2 = lists:filter( - fun - ("rabbit" ++ _) -> true; - (_) -> false - end, Names1), - case Names2 of - [First | _] -> - First; - [] -> - "rabbit" - end; - {error, address} -> - "rabbit" - end. - -complete_nodename(Nodename) -> - case re:run(Nodename, "@", [{capture, none}]) of - nomatch -> - {ok, ThisHost} = inet:gethostname(), - list_to_atom(Nodename ++ "@" ++ ThisHost); - match -> - list_to_atom(Nodename) - end. - -run_command( +run_remote_command( _Nodename, ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); -run_command(Nodename, _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> - erpc:call( - Nodename, +run_remote_command( + Connection, _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> + rabbit_cli_transport:rpc( + Connection, rabbit_cli_commands, run_command, [Progname, ArgMap, CmdPath, Command, IO]). + +run_local_command( + ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> + rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); +run_local_command( + _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> + rabbit_cli_commands:run_command( + Progname, ArgMap, CmdPath, Command, IO). diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl new file mode 100644 index 000000000000..0d5130aa32ab --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -0,0 +1,90 @@ +-module(rabbit_cli_transport). + +-export([connect/1, + rpc/4]). + +-record(http, {uri :: uri_string:uri_map(), + gun :: pid()}). + +connect(#{node := NodenameOrUri} = ArgMap) -> + case re:run(NodenameOrUri, "://", [{capture, none}]) of + nomatch -> + connect_using_erldist(ArgMap); + match -> + connect_using_other_proto(ArgMap) + end; +connect(ArgMap) -> + connect_using_erldist(ArgMap). + +rpc(Nodename, Mod, Func, Args) when is_atom(Nodename) -> + rpc_using_erldist(Nodename, Mod, Func, Args). + +%% ------------------------------------------------------------------- +%% Erlang distribution. +%% ------------------------------------------------------------------- + +connect_using_erldist(#{node := Nodename}) -> + do_connect_using_erldist(Nodename); +connect_using_erldist(_ArgMap) -> + GuessedNodename = guess_rabbitmq_nodename(), + do_connect_using_erldist(GuessedNodename). + +do_connect_using_erldist(Nodename) -> + maybe + Nodename1 = complete_nodename(Nodename), + {ok, _} ?= net_kernel:start( + undefined, #{name_domain => shortnames}), + + %% Can we reach the remote node? + case net_kernel:connect_node(Nodename1) of + true -> + {ok, Nodename1}; + false -> + {error, noconnection} + end + end. + +guess_rabbitmq_nodename() -> + case net_adm:names() of + {ok, NamesAndPorts} -> + Names0 = [Name || {Name, _Port} <- NamesAndPorts], + Names1 = lists:sort(Names0), + Names2 = lists:filter( + fun + ("rabbit" ++ _) -> true; + (_) -> false + end, Names1), + case Names2 of + [First | _] -> + First; + [] -> + "rabbit" + end; + {error, address} -> + "rabbit" + end. + +complete_nodename(Nodename) -> + case re:run(Nodename, "@", [{capture, none}]) of + nomatch -> + {ok, ThisHost} = inet:gethostname(), + list_to_atom(Nodename ++ "@" ++ ThisHost); + match -> + list_to_atom(Nodename) + end. + +rpc_using_erldist(Nodename, Mod, Func, Args) -> + erpc:call(Nodename, Mod, Func, Args). + +%% ------------------------------------------------------------------- +%% HTTP(S) transport. +%% ------------------------------------------------------------------- + +connect_using_other_proto(#{node := Uri}) -> + maybe + #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), + {ok, Gun} ?= gun:open(Host, Port), + State = #http{uri = UriMap, + gun = Gun}, + {ok, State} + end. From b622a444977e477cde539e251195dc9963977e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Dec 2024 13:56:07 +0100 Subject: [PATCH 03/51] Transport over websocket --- deps/rabbit/Makefile | 2 +- deps/rabbit/src/rabbit.erl | 1 + deps/rabbit/src/rabbit_cli.erl | 8 +- deps/rabbit/src/rabbit_cli_io.erl | 7 ++ deps/rabbit/src/rabbit_cli_transport.erl | 149 ++++++++++++++++++++--- deps/rabbit/src/rabbit_cli_ws.erl | 105 ++++++++++++++++ deps/rabbit/src/rabbit_cli_ws_runner.erl | 63 ++++++++++ 7 files changed, 310 insertions(+), 25 deletions(-) create mode 100644 deps/rabbit/src/rabbit_cli_ws.erl create mode 100644 deps/rabbit/src/rabbit_cli_ws_runner.erl diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index 7a6d502c00f2..be293f8cf324 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -129,7 +129,7 @@ endef LOCAL_DEPS = sasl os_mon inets compiler public_key crypto ssl syntax_tools xmerl BUILD_DEPS = rabbitmq_cli -DEPS = ranch cowlib rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat horus khepri khepri_mnesia_migration cuttlefish gen_batch_server +DEPS = ranch cowlib rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat horus khepri khepri_mnesia_migration cuttlefish gen_batch_server cowboy gun TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_client rabbitmq_amqp_client rabbitmq_amqp1_0 # We pin a version of Horus even if we don't use it directly (it is a diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index 3657f60f05bd..678921a8933c 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -951,6 +951,7 @@ start(normal, []) -> %% will be used. We start it now because we can't wait for boot steps %% to do this (feature flags are refreshed before boot steps run). ok = rabbit_sup:start_child(rabbit_ff_controller), + ok = rabbit_sup:start_child(rabbit_cli_ws), %% Compatibility with older RabbitMQ versions + required by %% rabbit_node_monitor:notify_node_up/0: diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index e0c263a79a56..833d3b1214ed 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -31,7 +31,7 @@ parse_command_pass1(Progname, Args, IO) -> PartialCmdPath, PartialCommand} ?= initial_parse(Progname, Args, PartialArgparseDef), - case rabbit_cli_transport:connect(PartialArgMap) of + case rabbit_cli_transport:connect(PartialArgMap, IO) of {ok, Connection} -> %% We can query the argparse definition from the remote node %% to know the commands it supports and proceed with the @@ -139,11 +139,11 @@ run_remote_command( _Nodename, ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); run_remote_command( - Connection, _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> - rabbit_cli_transport:rpc( + Connection, _ArgparseDef, Progname, ArgMap, CmdPath, Command, _IO) -> + rabbit_cli_transport:rpc_with_io( Connection, rabbit_cli_commands, run_command, - [Progname, ArgMap, CmdPath, Command, IO]). + [Progname, ArgMap, CmdPath, Command]). run_local_command( ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index f95efec0d884..0116d63f2ef8 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -53,15 +53,22 @@ argparse_def(record_stream) -> display_help(IO, CmdPath, Command) -> gen_server:cast(IO, {?FUNCTION_NAME, CmdPath, Command}). +start_record_stream({transport, Transport}, Name, Fields, ArgMap) -> + Transport ! {io_call, self(), {?FUNCTION_NAME, Name, Fields, ArgMap}}, + receive Ret -> Ret end; start_record_stream(IO, Name, Fields, ArgMap) when is_pid(IO) andalso is_atom(Name) andalso is_map(ArgMap) -> gen_server:call(IO, {?FUNCTION_NAME, Name, Fields, ArgMap}). +push_new_record({transport, Transport}, #{name := Name}, Record) -> + Transport ! {io_cast, {?FUNCTION_NAME, Name, Record}}; push_new_record(IO, #{name := Name}, Record) -> gen_server:cast(IO, {?FUNCTION_NAME, Name, Record}). +end_record_stream({transport, Transport}, #{name := Name}) -> + Transport ! {io_cast, {?FUNCTION_NAME, Name}}; end_record_stream(IO, #{name := Name}) -> gen_server:cast(IO, {?FUNCTION_NAME, Name}). diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl index 0d5130aa32ab..91ffef3ff477 100644 --- a/deps/rabbit/src/rabbit_cli_transport.erl +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -1,35 +1,55 @@ -module(rabbit_cli_transport). +-behaviour(gen_server). --export([connect/1, - rpc/4]). +-export([connect/2, + rpc/4, + rpc_with_io/4]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + config_change/3]). -record(http, {uri :: uri_string:uri_map(), - gun :: pid()}). + conn :: pid(), + stream :: gun:stream_ref(), + stream_ready = false :: boolean(), + pending = [] :: [any()], + io :: pid() + }). -connect(#{node := NodenameOrUri} = ArgMap) -> +connect(#{node := NodenameOrUri} = ArgMap, IO) -> case re:run(NodenameOrUri, "://", [{capture, none}]) of nomatch -> - connect_using_erldist(ArgMap); + connect_using_erldist(ArgMap, IO); match -> - connect_using_other_proto(ArgMap) + connect_using_transport(ArgMap, IO) end; -connect(ArgMap) -> - connect_using_erldist(ArgMap). +connect(ArgMap, IO) -> + connect_using_erldist(ArgMap, IO). -rpc(Nodename, Mod, Func, Args) when is_atom(Nodename) -> - rpc_using_erldist(Nodename, Mod, Func, Args). +rpc({Nodename, _IO} = Connection, Mod, Func, Args) when is_atom(Nodename) -> + rpc_using_erldist(Connection, Mod, Func, Args); +rpc(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> + rpc_using_transport(TransportPid, Mod, Func, Args). + +rpc_with_io({Nodename, _IO} = Connection, Mod, Func, Args) when is_atom(Nodename) -> + rpc_with_io_using_erldist(Connection, Mod, Func, Args); +rpc_with_io(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> + rpc_with_io_using_transport(TransportPid, Mod, Func, Args). %% ------------------------------------------------------------------- %% Erlang distribution. %% ------------------------------------------------------------------- -connect_using_erldist(#{node := Nodename}) -> - do_connect_using_erldist(Nodename); -connect_using_erldist(_ArgMap) -> +connect_using_erldist(#{node := Nodename}, IO) -> + do_connect_using_erldist(Nodename, IO); +connect_using_erldist(_ArgMap, IO) -> GuessedNodename = guess_rabbitmq_nodename(), - do_connect_using_erldist(GuessedNodename). + do_connect_using_erldist(GuessedNodename, IO). -do_connect_using_erldist(Nodename) -> +do_connect_using_erldist(Nodename, IO) -> maybe Nodename1 = complete_nodename(Nodename), {ok, _} ?= net_kernel:start( @@ -38,7 +58,7 @@ do_connect_using_erldist(Nodename) -> %% Can we reach the remote node? case net_kernel:connect_node(Nodename1) of true -> - {ok, Nodename1}; + {ok, {Nodename1, IO}}; false -> {error, noconnection} end @@ -73,18 +93,107 @@ complete_nodename(Nodename) -> list_to_atom(Nodename) end. -rpc_using_erldist(Nodename, Mod, Func, Args) -> +rpc_using_erldist({Nodename, _IO}, Mod, Func, Args) -> erpc:call(Nodename, Mod, Func, Args). +rpc_with_io_using_erldist({Nodename, IO}, Mod, Func, Args) -> + erpc:call(Nodename, Mod, Func, Args ++ [IO]). + %% ------------------------------------------------------------------- %% HTTP(S) transport. %% ------------------------------------------------------------------- -connect_using_other_proto(#{node := Uri}) -> +connect_using_transport(ArgMap, IO) -> + gen_server:start_link(?MODULE, {ArgMap, IO}, []). + +rpc_using_transport(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> + gen_server:call(TransportPid, {rpc, {Mod, Func, Args}, #{io => false}}). + +rpc_with_io_using_transport(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> + gen_server:call(TransportPid, {rpc, {Mod, Func, Args}, #{io => true}}). + +init({#{node := Uri}, IO}) -> maybe + {ok, _} ?= application:ensure_all_started(gun), #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), - {ok, Gun} ?= gun:open(Host, Port), + {ok, ConnPid} ?= gun:open(Host, Port), State = #http{uri = UriMap, - gun = Gun}, + conn = ConnPid, + io = IO}, + %logger:alert("Transport: State=~p", [State]), {ok, State} end. + +handle_call( + Request, From, + #http{stream_ready = true} = State) -> + send_call(Request, From, State), + {noreply, State}; +handle_call( + Request, From, + #http{stream_ready = false, pending = Pending} = State) -> + %logger:alert("Transport(call): ~p", [Request]), + State1 = State#http{pending = [{From, Request} | Pending]}, + {noreply, State1}; +handle_call(_Request, _From, State) -> + %logger:alert("Transport(call): ~p", [_Request]), + {reply, ok, State}. + +handle_cast(_Request, State) -> + %logger:alert("Transport(cast): ~p", [_Request]), + {noreply, State}. + +handle_info( + {gun_up, ConnPid, _}, + #http{conn = ConnPid} = State) -> + %logger:alert("Transport(info): Conn up"), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + State1 = State#http{stream = StreamRef}, + {noreply, State1}; +handle_info( + {gun_upgrade, ConnPid, StreamRef, _Frames, _}, + #http{conn = ConnPid, stream = StreamRef, pending = Pending} = State) -> + %logger:alert("Transport(info): WS upgraded, ~p", [_Frames]), + State1 = State#http{stream_ready = true, pending = []}, + Pending1 = lists:reverse(Pending), + lists:foreach( + fun({From, Request}) -> + send_call(Request, From, State1) + end, Pending1), + {noreply, State1}; +handle_info( + {gun_ws, ConnPid, StreamRef, {binary, ReplyBin}}, + #http{conn = ConnPid, stream = StreamRef, io = IO} = State) -> + Reply = binary_to_term(ReplyBin), + case Reply of + {io_call, From, Msg} -> + %logger:alert("IO call from WS: ~p -> ~p", [Msg, From]), + Ret = gen_server:call(IO, Msg), + RequestBin = term_to_binary({io_reply, From, Ret}), + Frame = {binary, RequestBin}, + gun:ws_send(ConnPid, StreamRef, Frame); + {io_cast, Msg} -> + %logger:alert("IO cast from WS: ~p", [Msg]), + gen_server:cast(IO, Msg); + {ret, From, Ret} -> + %logger:alert("Reply from WS: ~p -> ~p", [Ret, From]), + gen_server:reply(From, Ret); + _Other -> + %logger:alert("Reply from WS: ~p", [_Other]), + ok + end, + {noreply, State}; +handle_info(_Info, State) -> + %logger:alert("Transport(info): ~p", [_Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +config_change(_OldVsn, State, _Extra) -> + {ok, State}. + +send_call(Request, From, #http{conn = ConnPid, stream = StreamRef}) -> + RequestBin = term_to_binary({call, From, Request}), + Frame = {binary, RequestBin}, + gun:ws_send(ConnPid, StreamRef, Frame). diff --git a/deps/rabbit/src/rabbit_cli_ws.erl b/deps/rabbit/src/rabbit_cli_ws.erl new file mode 100644 index 000000000000..44ee7e27cc5b --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_ws.erl @@ -0,0 +1,105 @@ +-module(rabbit_cli_ws). +-behaviour(gen_server). +-behaviour(cowboy_websocket). + +-export([start_link/0]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + config_change/3]). +-export([init/2, + websocket_init/1, + websocket_handle/2, + websocket_info/2, + terminate/3]). + +start_link() -> + gen_server:start_link(?MODULE, #{}, []). + +init(_) -> + process_flag(trap_exit, true), + Dispatch = cowboy_router:compile( + [{'_', [{'_', ?MODULE, #{}}]}]), + {ok, _} = cowboy:start_clear(cli_ws_listener, + [{port, 8080}], + #{env => #{dispatch => Dispatch}} + ), + {ok, ok}. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + logger:alert("WS/gen_server: ~p", [_Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + logger:alert("WS/gen_server(terminate): ~p", [_Reason]), + ok = cowboy:stop_listener(cli_ws_listener), + ok. + +config_change(_OldVsn, State, _Extra) -> + {ok, State}. + +init(Req, State) -> + logger:alert("WS: Req=~p", [Req]), + {cowboy_websocket, Req, State, #{idle_timeout => 30000}}. + +websocket_init(State) -> + {ok, Runner} = rabbit_cli_ws_runner:start_link( + self(), {transport, self()}), + State1 = State#{runner => Runner}, + {ok, State1}. + +websocket_handle({binary, RequestBin}, State) -> + Request = binary_to_term(RequestBin), + case Request of + {io_reply, From, Ret} -> + From ! Ret, + {ok, State}; + {call, From, Call} -> + handle_ws_call(Call, From, State), + {ok, State}; + _ -> + logger:alert("Unknown request: ~p", [Request]), + ReplyBin = term_to_binary({error, Request}), + Frame = {binary, ReplyBin}, + {[Frame], State} + end; +websocket_handle(_Frame, State) -> + logger:alert("Frame: ~p", [_Frame]), + {ok, State}. + +websocket_info({io_call, _From, _Msg} = Call, State) -> + ReplyBin = term_to_binary(Call), + Frame = {binary, ReplyBin}, + {[Frame], State}; +websocket_info({io_cast, _Msg} = Call, State) -> + ReplyBin = term_to_binary(Call), + Frame = {binary, ReplyBin}, + {[Frame], State}; +websocket_info({reply, Ret, From}, State) -> + logger:alert("WS/cowboy: ~p", [Ret]), + ReplyBin = term_to_binary({ret, From, Ret}), + Frame = {binary, ReplyBin}, + {[Frame], State}; +websocket_info(_Info, State) -> + logger:alert("WS/cowboy: ~p", [_Info]), + {ok, State}. + +terminate(_Reason, _Req, #{runner := Runner}) -> + rabbit_cli_ws_runner:stop(Runner), + receive + {'EXIT', Runner, _} -> + ok + end, + ok. + +handle_ws_call(Call, From, #{runner := Runner}) -> + logger:alert("Call: ~p", [Call]), + gen_server:cast(Runner, {Call, From}). diff --git a/deps/rabbit/src/rabbit_cli_ws_runner.erl b/deps/rabbit/src/rabbit_cli_ws_runner.erl new file mode 100644 index 000000000000..d525f7fc3b7e --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_ws_runner.erl @@ -0,0 +1,63 @@ +-module(rabbit_cli_ws_runner). +-behaviour(gen_server). + +-export([start_link/2, + stop/1]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + config_change/3]). + +start_link(WS, IO) -> + gen_server:start_link(?MODULE, #{ws => WS, io => IO}, []). + +stop(Runner) -> + gen_server:stop(Runner). + +init(#{ws := _, io := _} = Args) -> + process_flag(trap_exit, true), + {ok, Args}. + +handle_call(_Request, _From, State) -> + logger:alert("Runner(call): ~p", [_Request]), + {reply, ok, State}. + +handle_cast( + {{rpc, {Mod, Func, Args}, Options}, From}, + #{ws := WS, io := IO} = State) -> + try + Args1 = case Options of + #{io := true} -> + Args ++ [IO]; + _ -> + Args + end, + Ret = erlang:apply(Mod, Func, Args1), + logger:alert("Runner(rpc): ~p", [Ret]), + WS ! {reply, Ret, From}, + {noreply, State} + catch + Class:Reason:Stacktrace -> + Ex = {exception, Class, Reason, Stacktrace}, + WS ! {reply, Ex, From}, + {noreply, State} + end; +handle_cast(_Request, State) -> + logger:alert("Runner(cast): ~p", [_Request]), + {noreply, State}. + +handle_info({'EXIT', WS, _Reason}, #{ws := WS} = State) -> + {stop, State}; +handle_info({'EXIT', IO, _Reason}, #{io := IO} = State) -> + {stop, State}; +handle_info(_Info, State) -> + logger:alert("Runner/gen_server: ~p, ~p", [_Info, State]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +config_change(_OldVsn, State, _Extra) -> + {ok, State}. From 20108e78ee2f5fe884471a889523efa7503f62f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Dec 2024 17:05:02 +0100 Subject: [PATCH 04/51] Add command alias support --- deps/rabbit/src/rabbit_cli.erl | 63 ++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index 833d3b1214ed..62ca52ebf4b7 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -17,13 +17,13 @@ run_cli(Args) -> {ok, IO} ?= rabbit_cli_io:start_link(Progname), try - parse_command_pass1(Progname, Args, IO) + do_run_cli(Progname, Args, IO) after rabbit_cli_io:stop(IO) end end. -parse_command_pass1(Progname, Args, IO) -> +do_run_cli(Progname, Args, IO) -> maybe PartialArgparseDef = argparse_def(), {ok, @@ -37,7 +37,8 @@ parse_command_pass1(Progname, Args, IO) -> %% to know the commands it supports and proceed with the %% execution. maybe - ArgparseDef = get_final_argparse_def(Connection), + ArgparseDef = get_final_argparse_def( + Connection, PartialArgparseDef), {ok, ArgMap, CmdPath, @@ -71,6 +72,15 @@ add_rabbitmq_code_path(Progname) -> ok. argparse_def() -> + Aliases0 = #{"lx" => "list_exchanges -v", + "list_exchanges" => "list exchanges"}, + Aliases1 = maps:map( + fun(Alias, CommandStr) -> + #{help => <<"alias">>, + handler => fun(ArgMap) -> + handle_alias(Alias, CommandStr, ArgMap) + end} + end, Aliases0), #{arguments => [ #{name => help, @@ -97,7 +107,7 @@ argparse_def() -> "Display version and exit"} ], - commands => #{}}. + commands => Aliases1}. initial_parse(Progname, Args, ArgparseDef) -> Options = #{progname => Progname}, @@ -124,17 +134,41 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> Error end. -get_final_argparse_def(Connection) -> - ArgparseDef1 = argparse_def(), +get_final_argparse_def(Connection, PartialArgparseDef) -> + ArgparseDef1 = PartialArgparseDef, ArgparseDef2 = rabbit_cli_transport:rpc( Connection, rabbit_cli_commands, argparse_def, []), - ArgparseDef = maps:merge(ArgparseDef1, ArgparseDef2), + ArgparseDef = merge_argparse_def(ArgparseDef1, ArgparseDef2), ArgparseDef. +merge_argparse_def(ArgparseDef1, ArgparseDef2) -> + Args1 = maps:get(arguments, ArgparseDef1, []), + Args2 = maps:get(arguments, ArgparseDef2, []), + Args = merge_arguments(Args1, Args2), + Cmds1 = maps:get(commands, ArgparseDef1, #{}), + Cmds2 = maps:get(commands, ArgparseDef2, #{}), + Cmds = merge_commands(Cmds1, Cmds2), + maps:merge(ArgparseDef1, ArgparseDef2#{arguments => Args, commands => Cmds}). + +merge_arguments(Args1, Args2) -> + Args1 ++ Args2. + +merge_commands(Cmds1, Cmds2) -> + maps:merge(Cmds1, Cmds2). + final_parse(Progname, Args, ArgparseDef) -> Options = #{progname => Progname}, argparse:parse(Args, ArgparseDef, Options). +run_remote_command( + Connection, ArgparseDef, Progname, ArgMap, _CmdPath, + #{help := <<"alias">>} = Command, + IO) -> + {ok, ArgMap1, CmdPath1, Command1} = expand_alias( + ArgparseDef, Progname, ArgMap, + Command), + run_remote_command( + Connection, ArgparseDef, Progname, ArgMap1, CmdPath1, Command1, IO); run_remote_command( _Nodename, ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); @@ -152,3 +186,18 @@ run_local_command( _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> rabbit_cli_commands:run_command( Progname, ArgMap, CmdPath, Command, IO). + +handle_alias(Alias, CommandStr, ArgMap) -> + {alias, Alias, CommandStr, ArgMap}. + +expand_alias(ArgparseDef, Progname, ArgMap, #{handler := Fun} = _Command) -> + {alias, _Alias, CommandStr, ArgMap} = Fun(ArgMap), + Args = string:lexemes(CommandStr, " "), + Options = #{progname => Progname}, + case argparse:parse(Args, ArgparseDef, Options) of + {ok, ArgMap1, CmdPath1, Command1} -> + ArgMap2 = maps:merge(ArgMap1, ArgMap), + {ok, ArgMap2, CmdPath1, Command1}; + {error, _} = Error -> + Error + end. From 47b342bb8c154757295b8440f71abc993306dd68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 31 Dec 2024 15:38:14 +0100 Subject: [PATCH 05/51] Improve transport and alias handling + discover commands --- deps/rabbit/src/rabbit_cli.erl | 146 +++++++++++------------ deps/rabbit/src/rabbit_cli_commands.erl | 116 ++++++++++++++---- deps/rabbit/src/rabbit_cli_io.erl | 8 +- deps/rabbit/src/rabbit_cli_transport.erl | 56 ++++----- deps/rabbit/src/rabbit_cli_ws_runner.erl | 25 ++-- deps/rabbit_common/include/logging.hrl | 1 + 6 files changed, 216 insertions(+), 136 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index 62ca52ebf4b7..91c2fd5e3c45 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -2,7 +2,10 @@ -include_lib("kernel/include/logger.hrl"). --export([main/1]). +-export([main/1, + merge_argparse_def/2, + handle_alias/1, + noop/1]). main(Args) -> Ret = run_cli(Args), @@ -24,39 +27,40 @@ run_cli(Args) -> end. do_run_cli(Progname, Args, IO) -> + PartialArgparseDef = argparse_def(), + Context0 = #{progname => Progname, + args => Args, + io => IO, + argparse_def => PartialArgparseDef}, maybe - PartialArgparseDef = argparse_def(), {ok, PartialArgMap, PartialCmdPath, - PartialCommand} ?= initial_parse(Progname, Args, PartialArgparseDef), - - case rabbit_cli_transport:connect(PartialArgMap, IO) of - {ok, Connection} -> - %% We can query the argparse definition from the remote node - %% to know the commands it supports and proceed with the - %% execution. - maybe - ArgparseDef = get_final_argparse_def( - Connection, PartialArgparseDef), - {ok, - ArgMap, - CmdPath, - Command} ?= final_parse(Progname, Args, ArgparseDef), - - run_remote_command( - Connection, ArgparseDef, - Progname, ArgMap, CmdPath, Command, - IO) - end; - {error, _} -> - %% We can't reach the remote node. Let's fallback - %% to a local execution. - run_local_command( - PartialArgparseDef, - Progname, PartialArgMap, PartialCmdPath, - PartialCommand, IO) - end + PartialCommand} ?= initial_parse(Context0), + Context1 = Context0#{arg_map => PartialArgMap, + cmd_path => PartialCmdPath, + command => PartialCommand}, + + Context2 = case rabbit_cli_transport:connect(Context1) of + {ok, Connection} -> + Context1#{connection => Connection}; + {error, _} -> + Context1 + end, + + %% We can query the argparse definition from the remote node to know + %% the commands it supports and proceed with the execution. + ArgparseDef = get_final_argparse_def(Context2), + Context3 = Context2#{argparse_def => ArgparseDef}, + {ok, + ArgMap, + CmdPath, + Command} ?= final_parse(Context3), + Context4 = Context3#{arg_map => ArgMap, + cmd_path => CmdPath, + command => Command}, + + run_command(Context4) end. add_rabbitmq_code_path(Progname) -> @@ -75,11 +79,11 @@ argparse_def() -> Aliases0 = #{"lx" => "list_exchanges -v", "list_exchanges" => "list exchanges"}, Aliases1 = maps:map( - fun(Alias, CommandStr) -> - #{help => <<"alias">>, - handler => fun(ArgMap) -> - handle_alias(Alias, CommandStr, ArgMap) - end} + fun(_Alias, CommandStr) -> + Args = string:lexemes(CommandStr, " "), + #{alias => Args, + help => hidden, + handler => {?MODULE, handle_alias}} end, Aliases0), #{arguments => [ @@ -107,9 +111,11 @@ argparse_def() -> "Display version and exit"} ], - commands => Aliases1}. + commands => Aliases1, + handler => {?MODULE, noop}}. -initial_parse(Progname, Args, ArgparseDef) -> +initial_parse( + #{progname := Progname, args := Args, argparse_def := ArgparseDef}) -> Options = #{progname => Progname}, case partial_parse(Args, ArgparseDef, Options) of {ok, ArgMap, CmdPath, Command, _RemainingArgs} -> @@ -119,7 +125,8 @@ initial_parse(Progname, Args, ArgparseDef) -> end. partial_parse(Args, ArgparseDef, Options) -> - partial_parse(Args, ArgparseDef, Options, []). + ArgparseDef1 = maps:remove(commands, ArgparseDef), + partial_parse(Args, ArgparseDef1, Options, []). partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> case argparse:parse(Args, ArgparseDef, Options) of @@ -134,7 +141,8 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> Error end. -get_final_argparse_def(Connection, PartialArgparseDef) -> +get_final_argparse_def( + #{connection := Connection, argparse_def := PartialArgparseDef}) -> ArgparseDef1 = PartialArgparseDef, ArgparseDef2 = rabbit_cli_transport:rpc( Connection, rabbit_cli_commands, argparse_def, []), @@ -148,7 +156,9 @@ merge_argparse_def(ArgparseDef1, ArgparseDef2) -> Cmds1 = maps:get(commands, ArgparseDef1, #{}), Cmds2 = maps:get(commands, ArgparseDef2, #{}), Cmds = merge_commands(Cmds1, Cmds2), - maps:merge(ArgparseDef1, ArgparseDef2#{arguments => Args, commands => Cmds}). + maps:merge( + ArgparseDef1, + ArgparseDef2#{arguments => Args, commands => Cmds}). merge_arguments(Args1, Args2) -> Args1 ++ Args2. @@ -156,48 +166,34 @@ merge_arguments(Args1, Args2) -> merge_commands(Cmds1, Cmds2) -> maps:merge(Cmds1, Cmds2). -final_parse(Progname, Args, ArgparseDef) -> +final_parse( + #{progname := Progname, args := Args, argparse_def := ArgparseDef}) -> Options = #{progname => Progname}, argparse:parse(Args, ArgparseDef, Options). -run_remote_command( - Connection, ArgparseDef, Progname, ArgMap, _CmdPath, - #{help := <<"alias">>} = Command, - IO) -> - {ok, ArgMap1, CmdPath1, Command1} = expand_alias( - ArgparseDef, Progname, ArgMap, - Command), - run_remote_command( - Connection, ArgparseDef, Progname, ArgMap1, CmdPath1, Command1, IO); -run_remote_command( - _Nodename, ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> - rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); -run_remote_command( - Connection, _ArgparseDef, Progname, ArgMap, CmdPath, Command, _IO) -> - rabbit_cli_transport:rpc_with_io( - Connection, - rabbit_cli_commands, run_command, - [Progname, ArgMap, CmdPath, Command]). - -run_local_command( - ArgparseDef, _Progname, #{help := true}, CmdPath, _Command, IO) -> - rabbit_cli_io:display_help(IO, CmdPath, ArgparseDef); -run_local_command( - _ArgparseDef, Progname, ArgMap, CmdPath, Command, IO) -> - rabbit_cli_commands:run_command( - Progname, ArgMap, CmdPath, Command, IO). - -handle_alias(Alias, CommandStr, ArgMap) -> - {alias, Alias, CommandStr, ArgMap}. - -expand_alias(ArgparseDef, Progname, ArgMap, #{handler := Fun} = _Command) -> - {alias, _Alias, CommandStr, ArgMap} = Fun(ArgMap), - Args = string:lexemes(CommandStr, " "), +run_command(#{arg_map := #{help := true}} = Context) -> + rabbit_cli_io:display_help(Context); +run_command(#{connection := Connection} = Context) -> + rabbit_cli_transport:run_command(Connection, Context); +run_command(Context) -> + rabbit_cli_commands:run_command(Context). + +noop(_Context) -> + ok. + +handle_alias( + #{progname := Progname, + argparse_def := ArgparseDef, + arg_map := ArgMap, + command := #{alias := Args}} = Context) -> Options = #{progname => Progname}, case argparse:parse(Args, ArgparseDef, Options) of {ok, ArgMap1, CmdPath1, Command1} -> ArgMap2 = maps:merge(ArgMap1, ArgMap), - {ok, ArgMap2, CmdPath1, Command1}; + Context1 = Context#{arg_map => ArgMap2, + cmd_path => CmdPath1, + command => Command1}, + rabbit_cli_commands:do_run_command(Context1); {error, _} = Error -> Error end. diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 6ef13d55cef1..82572adddd89 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -2,40 +2,114 @@ -include_lib("kernel/include/logger.hrl"). --export([argparse_def/0, run_command/5]). --export([list_exchanges/5]). +-include_lib("rabbit_common/include/logging.hrl"). + +-export([argparse_def/0, run_command/1, do_run_command/1]). +-export([cmd_list_exchanges/1]). + +-rabbitmq_command( + {#{cli => ["declare", "exchange"], + http => {put, ["exchanges", vhost, exchange]}}, + #{help => "Declare new exchange", + arguments => [ + #{name => vhost, + long => "-vhost", + type => binary, + default => <<"/">>, + help => "Name of the vhost owning the new exchange"}, + #{name => exchange, + type => binary, + help => "Name of the exchange to declare"} + ], + handler => {?MODULE, cmd_declare_exchange}}}). + +-rabbitmq_command( + {#{cli => ["list", "exchanges"], + http => {get, ["exchanges"]}}, + [argparse_def_record_stream, + #{help => "List exchanges", + handler => {?MODULE, cmd_list_exchanges}}]}). argparse_def() -> + #{argparse_def := ArgparseDef} = get_discovered_commands(), + ArgparseDef. + +get_discovered_commands() -> + Key = {?MODULE, discovered_commands}, + try + persistent_term:get(Key) + catch + error:badarg -> + Commands = discover_commands(), + ArgparseDef = commands_to_cli_argparse_def(Commands), + Cache = #{commands => Commands, + argparse_def => ArgparseDef}, + persistent_term:put(Key, Cache), + Cache + end. + +discover_commands() -> %% Extract the commands from module attributes like feature flags and boot %% steps. - #{commands => - #{"list" => - #{help => "List entities", - commands => - #{"exchanges" => - maps:merge( - rabbit_cli_io:argparse_def(record_stream), - #{help => "List exchanges", - handler => {?MODULE, list_exchanges}}) - } - } - } - }. + ?LOG_DEBUG( + "Commands: query commands in loaded applications", + #{domain => ?RMQLOG_DOMAIN_CMD}), + T0 = erlang:monotonic_time(), + ScannedApps = rabbit_misc:rabbitmq_related_apps(), + AttrsPerApp = rabbit_misc:module_attributes_from_apps( + rabbitmq_command, ScannedApps), + T1 = erlang:monotonic_time(), + ?LOG_DEBUG( + "Commands: time to find supported commands: ~tp us", + [erlang:convert_time_unit(T1 - T0, native, microsecond)], + #{domain => ?RMQLOG_DOMAIN_CMD}), + AttrsPerApp. + +commands_to_cli_argparse_def(Commands) -> + lists:foldl( + fun({_App, _Mod, Entries}, Acc0) -> + lists:foldl( + fun + ({#{cli := Path}, Def}, Acc1) -> + Def1 = expand_argparse_def(Def), + M1 = lists:foldr( + fun + (Cmd, undefined) -> + #{commands => #{Cmd => Def1}}; + (Cmd, M0) -> + #{commands => #{Cmd => M0}} + end, undefined, Path), + rabbit_cli:merge_argparse_def(Acc1, M1); + (_, Acc1) -> + Acc1 + end, Acc0, Entries) + end, #{}, Commands). + +expand_argparse_def(Def) when is_map(Def) -> + Def; +expand_argparse_def(Defs) when is_list(Defs) -> + lists:foldl( + fun(argparse_def_record_stream, Acc) -> + Def = rabbit_cli_io:argparse_def(record_stream), + rabbit_cli:merge_argparse_def(Acc, Def); + (Def, Acc) -> + Def1 = expand_argparse_def(Def), + rabbit_cli:merge_argparse_def(Acc, Def1) + end, #{}, Defs). -run_command(Progname, ArgMap, CmdPath, Command, IO) -> +run_command(Context) -> %% TODO: Put both processes under the rabbit supervision tree. - RunnerPid = command_runner(Progname, ArgMap, CmdPath, Command, IO), + RunnerPid = spawn_link(fun() -> do_run_command(Context) end), RunnerMRef = erlang:monitor(process, RunnerPid), receive {'DOWN', RunnerMRef, _, _, Reason} -> {ok, Reason} end. -command_runner( - Progname, ArgMap, CmdPath, #{handler := {Mod, Fun}} = Command, IO) -> - spawn_link(Mod, Fun, [Progname, ArgMap, CmdPath, Command, IO]). +do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> + erlang:apply(Mod, Fun, [Context]). -list_exchanges(_Progname, ArgMap, _CmdPath, _Command, IO) -> +cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> InfoKeys = rabbit_exchange:info_keys(), Fields = lists:map( fun diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 0116d63f2ef8..7343481605a6 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -7,7 +7,7 @@ -export([start_link/1, stop/1, argparse_def/1, - display_help/3, + display_help/1, start_record_stream/4, push_new_record/3, end_record_stream/2]). @@ -50,8 +50,8 @@ argparse_def(record_stream) -> ] }. -display_help(IO, CmdPath, Command) -> - gen_server:cast(IO, {?FUNCTION_NAME, CmdPath, Command}). +display_help(#{io := IO} = Context) -> + gen_server:cast(IO, {?FUNCTION_NAME, Context}). start_record_stream({transport, Transport}, Name, Fields, ArgMap) -> Transport ! {io_call, self(), {?FUNCTION_NAME, Name, Fields, ArgMap}}, @@ -98,7 +98,7 @@ handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast( - {display_help, CmdPath, ArgparseDef}, + {display_help, #{cmd_path := CmdPath, argparse_def := ArgparseDef}}, #?MODULE{progname = Progname} = State) -> Options = #{progname => Progname, %% Work around bug in argparse; diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl index 91ffef3ff477..d10e24f0aac7 100644 --- a/deps/rabbit/src/rabbit_cli_transport.erl +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -1,9 +1,9 @@ -module(rabbit_cli_transport). -behaviour(gen_server). --export([connect/2, +-export([connect/1, rpc/4, - rpc_with_io/4]). + run_command/2]). -export([init/1, handle_call/3, handle_cast/2, @@ -19,37 +19,37 @@ io :: pid() }). -connect(#{node := NodenameOrUri} = ArgMap, IO) -> +connect(#{arg_map := #{node := NodenameOrUri}} = Context) -> case re:run(NodenameOrUri, "://", [{capture, none}]) of nomatch -> - connect_using_erldist(ArgMap, IO); + connect_using_erldist(Context); match -> - connect_using_transport(ArgMap, IO) + connect_using_transport(Context) end; -connect(ArgMap, IO) -> - connect_using_erldist(ArgMap, IO). +connect(Context) -> + connect_using_erldist(Context). -rpc({Nodename, _IO} = Connection, Mod, Func, Args) when is_atom(Nodename) -> - rpc_using_erldist(Connection, Mod, Func, Args); +rpc(Nodename, Mod, Func, Args) when is_atom(Nodename) -> + rpc_using_erldist(Nodename, Mod, Func, Args); rpc(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> rpc_using_transport(TransportPid, Mod, Func, Args). -rpc_with_io({Nodename, _IO} = Connection, Mod, Func, Args) when is_atom(Nodename) -> - rpc_with_io_using_erldist(Connection, Mod, Func, Args); -rpc_with_io(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> - rpc_with_io_using_transport(TransportPid, Mod, Func, Args). +run_command(Nodename, Context) when is_atom(Nodename) -> + run_command_using_erldist(Nodename, Context); +run_command(TransportPid, Context) when is_pid(TransportPid) -> + run_command_using_transport(TransportPid, Context). %% ------------------------------------------------------------------- %% Erlang distribution. %% ------------------------------------------------------------------- -connect_using_erldist(#{node := Nodename}, IO) -> - do_connect_using_erldist(Nodename, IO); -connect_using_erldist(_ArgMap, IO) -> +connect_using_erldist(#{arg_map := #{node := Nodename}}) -> + do_connect_using_erldist(Nodename); +connect_using_erldist(_Context) -> GuessedNodename = guess_rabbitmq_nodename(), - do_connect_using_erldist(GuessedNodename, IO). + do_connect_using_erldist(GuessedNodename). -do_connect_using_erldist(Nodename, IO) -> +do_connect_using_erldist(Nodename) -> maybe Nodename1 = complete_nodename(Nodename), {ok, _} ?= net_kernel:start( @@ -58,7 +58,7 @@ do_connect_using_erldist(Nodename, IO) -> %% Can we reach the remote node? case net_kernel:connect_node(Nodename1) of true -> - {ok, {Nodename1, IO}}; + {ok, Nodename1}; false -> {error, noconnection} end @@ -93,26 +93,26 @@ complete_nodename(Nodename) -> list_to_atom(Nodename) end. -rpc_using_erldist({Nodename, _IO}, Mod, Func, Args) -> +rpc_using_erldist(Nodename, Mod, Func, Args) -> erpc:call(Nodename, Mod, Func, Args). -rpc_with_io_using_erldist({Nodename, IO}, Mod, Func, Args) -> - erpc:call(Nodename, Mod, Func, Args ++ [IO]). +run_command_using_erldist(Nodename, Context) -> + erpc:call(Nodename, rabbit_cli_commands, run_command, [Context]). %% ------------------------------------------------------------------- %% HTTP(S) transport. %% ------------------------------------------------------------------- -connect_using_transport(ArgMap, IO) -> - gen_server:start_link(?MODULE, {ArgMap, IO}, []). +connect_using_transport(Context) -> + gen_server:start_link(?MODULE, Context, []). rpc_using_transport(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> - gen_server:call(TransportPid, {rpc, {Mod, Func, Args}, #{io => false}}). + gen_server:call(TransportPid, {rpc, {Mod, Func, Args}}). -rpc_with_io_using_transport(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> - gen_server:call(TransportPid, {rpc, {Mod, Func, Args}, #{io => true}}). +run_command_using_transport(TransportPid, Context) when is_pid(TransportPid) -> + gen_server:call(TransportPid, {run_command, Context}). -init({#{node := Uri}, IO}) -> +init(#{arg_map := #{node := Uri}, io := IO}) -> maybe {ok, _} ?= application:ensure_all_started(gun), #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), diff --git a/deps/rabbit/src/rabbit_cli_ws_runner.erl b/deps/rabbit/src/rabbit_cli_ws_runner.erl index d525f7fc3b7e..5ca7364b1b5b 100644 --- a/deps/rabbit/src/rabbit_cli_ws_runner.erl +++ b/deps/rabbit/src/rabbit_cli_ws_runner.erl @@ -25,16 +25,25 @@ handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast( - {{rpc, {Mod, Func, Args}, Options}, From}, + {{rpc, {Mod, Func, Args}}, From}, + #{ws := WS} = State) -> + try + Ret = erlang:apply(Mod, Func, Args), + logger:alert("Runner(rpc): ~p", [Ret]), + WS ! {reply, Ret, From}, + {noreply, State} + catch + Class:Reason:Stacktrace -> + Ex = {exception, Class, Reason, Stacktrace}, + WS ! {reply, Ex, From}, + {noreply, State} + end; +handle_cast( + {{run_command, Context}, From}, #{ws := WS, io := IO} = State) -> try - Args1 = case Options of - #{io := true} -> - Args ++ [IO]; - _ -> - Args - end, - Ret = erlang:apply(Mod, Func, Args1), + Context1 = Context#{io => IO}, + Ret = erlang:apply(rabbit_cli_commands, run_command, [Context1]), logger:alert("Runner(rpc): ~p", [Ret]), WS ! {reply, Ret, From}, {noreply, State} diff --git a/deps/rabbit_common/include/logging.hrl b/deps/rabbit_common/include/logging.hrl index 2b852b947cef..b2f11860d10b 100644 --- a/deps/rabbit_common/include/logging.hrl +++ b/deps/rabbit_common/include/logging.hrl @@ -3,6 +3,7 @@ -define(DEFINE_RMQLOG_DOMAIN(Domain), [?RMQLOG_SUPER_DOMAIN_NAME, Domain]). -define(RMQLOG_DOMAIN_CHAN, ?DEFINE_RMQLOG_DOMAIN(channel)). +-define(RMQLOG_DOMAIN_CMD, ?DEFINE_RMQLOG_DOMAIN(commands)). -define(RMQLOG_DOMAIN_CONN, ?DEFINE_RMQLOG_DOMAIN(connection)). -define(RMQLOG_DOMAIN_DB, ?DEFINE_RMQLOG_DOMAIN(db)). -define(RMQLOG_DOMAIN_FEAT_FLAGS, ?DEFINE_RMQLOG_DOMAIN(feature_flags)). From 6a9fcdbe7e6f8d032057ce3c16e3f0ea2c9a5d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 31 Dec 2024 15:49:30 +0100 Subject: [PATCH 06/51] Support --help with aliases --- deps/rabbit/src/rabbit_cli.erl | 2 -- deps/rabbit/src/rabbit_cli_commands.erl | 3 +++ deps/rabbit/src/rabbit_cli_io.erl | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index 91c2fd5e3c45..21332ea9ad5a 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -171,8 +171,6 @@ final_parse( Options = #{progname => Progname}, argparse:parse(Args, ArgparseDef, Options). -run_command(#{arg_map := #{help := true}} = Context) -> - rabbit_cli_io:display_help(Context); run_command(#{connection := Connection} = Context) -> rabbit_cli_transport:run_command(Connection, Context); run_command(Context) -> diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 82572adddd89..51581eb3882f 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -106,6 +106,9 @@ run_command(Context) -> {ok, Reason} end. +do_run_command(#{command := Command, arg_map := #{help := true}} = Context) + when not is_map_key(alias, Command) -> + rabbit_cli_io:display_help(Context); do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> erlang:apply(Mod, Fun, [Context]). diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 7343481605a6..8e4f21d7251d 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -50,6 +50,8 @@ argparse_def(record_stream) -> ] }. +display_help(#{io := {transport, Transport}} = Context) -> + Transport ! {io_cast, {?FUNCTION_NAME, Context}}; display_help(#{io := IO} = Context) -> gen_server:cast(IO, {?FUNCTION_NAME, Context}). From 547f225c06b4e16815a98e66d3ce0e3e737e71c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 2 Jan 2025 11:50:58 +0100 Subject: [PATCH 07/51] Add support to configure aliases --- deps/rabbit/priv/schema/rabbitmqctl.schema | 12 ++ deps/rabbit/src/rabbit_cli.erl | 161 ++++++++++++++++----- deps/rabbit/src/rabbit_cli_commands.erl | 25 +++- 3 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 deps/rabbit/priv/schema/rabbitmqctl.schema diff --git a/deps/rabbit/priv/schema/rabbitmqctl.schema b/deps/rabbit/priv/schema/rabbitmqctl.schema new file mode 100644 index 000000000000..2319fbecdaa4 --- /dev/null +++ b/deps/rabbit/priv/schema/rabbitmqctl.schema @@ -0,0 +1,12 @@ +%% vim:ft=erlang:sw=4:et: + +{mapping, "alias.$alias", "rabbitmqctl.aliases", + [{datatype, string}]}. + +{translation, "rabbitmqctl.aliases", + fun(Conf) -> + Aliases0 = cuttlefish_variable:filter_by_prefix("alias", Conf), + Aliases1 = [{Alias, Value} || {["alias", Alias], Value} <- Aliases0], + Aliases2 = rabbit_cli:translate_aliases(Aliases1), + Aliases2 + end}. diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index 21332ea9ad5a..ed67033a34db 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -1,9 +1,11 @@ -module(rabbit_cli). -include_lib("kernel/include/logger.hrl"). +-include_lib("stdlib/include/assert.hrl"). -export([main/1, merge_argparse_def/2, + translate_aliases/1, handle_alias/1, noop/1]). @@ -41,28 +43,35 @@ do_run_cli(Progname, Args, IO) -> cmd_path => PartialCmdPath, command => PartialCommand}, - Context2 = case rabbit_cli_transport:connect(Context1) of + {ok, Config} ?= read_config_file(Context1), + Context2 = Context1#{config => Config}, + + Context3 = case rabbit_cli_transport:connect(Context2) of {ok, Connection} -> - Context1#{connection => Connection}; + Context2#{connection => Connection}; {error, _} -> - Context1 + Context2 end, %% We can query the argparse definition from the remote node to know %% the commands it supports and proceed with the execution. - ArgparseDef = get_final_argparse_def(Context2), - Context3 = Context2#{argparse_def => ArgparseDef}, + {ok, ArgparseDef} ?= get_final_argparse_def(Context3), + Context4 = Context3#{argparse_def => ArgparseDef}, {ok, ArgMap, CmdPath, - Command} ?= final_parse(Context3), - Context4 = Context3#{arg_map => ArgMap, + Command} ?= final_parse(Context4), + Context5 = Context4#{arg_map => ArgMap, cmd_path => CmdPath, command => Command}, - run_command(Context4) + run_command(Context5) end. +%% ------------------------------------------------------------------- +%% RabbitMQ code directory. +%% ------------------------------------------------------------------- + add_rabbitmq_code_path(Progname) -> ScriptDir = filename:dirname(Progname), PluginsDir0 = filename:join([ScriptDir, "..", "plugins"]), @@ -75,16 +84,11 @@ add_rabbitmq_code_path(Progname) -> lists:foreach(fun code:add_path/1, AppDirs), ok. +%% ------------------------------------------------------------------- +%% Arguments definition and parsing. +%% ------------------------------------------------------------------- + argparse_def() -> - Aliases0 = #{"lx" => "list_exchanges -v", - "list_exchanges" => "list exchanges"}, - Aliases1 = maps:map( - fun(_Alias, CommandStr) -> - Args = string:lexemes(CommandStr, " "), - #{alias => Args, - help => hidden, - handler => {?MODULE, handle_alias}} - end, Aliases0), #{arguments => [ #{name => help, @@ -107,11 +111,11 @@ argparse_def() -> #{name => version, long => "-version", short => $V, + type => boolean, help => "Display version and exit"} ], - commands => Aliases1, handler => {?MODULE, noop}}. initial_parse( @@ -125,8 +129,7 @@ initial_parse( end. partial_parse(Args, ArgparseDef, Options) -> - ArgparseDef1 = maps:remove(commands, ArgparseDef), - partial_parse(Args, ArgparseDef1, Options, []). + partial_parse(Args, ArgparseDef, Options, []). partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> case argparse:parse(Args, ArgparseDef, Options) of @@ -141,13 +144,31 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> Error end. -get_final_argparse_def( - #{connection := Connection, argparse_def := PartialArgparseDef}) -> - ArgparseDef1 = PartialArgparseDef, - ArgparseDef2 = rabbit_cli_transport:rpc( - Connection, rabbit_cli_commands, argparse_def, []), - ArgparseDef = merge_argparse_def(ArgparseDef1, ArgparseDef2), - ArgparseDef. +get_final_argparse_def(#{argparse_def := PartialArgparseDef} = Context) -> + maybe + {ok, Aliases} ?= get_aliases(Context), + {ok, FullArgparseDef} ?= get_full_argparse_def(Context), + ArgparseDef1 = merge_argparse_def(PartialArgparseDef, Aliases), + ArgparseDef2 = merge_argparse_def(ArgparseDef1, FullArgparseDef), + {ok, ArgparseDef2} + end. + +get_aliases(#{config := Config}) -> + Aliases = maps:get(aliases, Config, #{}), + case Aliases =:= #{} of + true -> + {ok, #{}}; + false -> + {ok, #{commands => Aliases}} + end. + +get_full_argparse_def(#{connection := Connection}) -> + RemoteArgparseDef = rabbit_cli_transport:rpc( + Connection, rabbit_cli_commands, argparse_def, []), + {ok, RemoteArgparseDef}; +get_full_argparse_def(_) -> + LocalArgparseDef = rabbit_cli_commands:argparse_def(), + {ok, LocalArgparseDef}. merge_argparse_def(ArgparseDef1, ArgparseDef2) -> Args1 = maps:get(arguments, ArgparseDef1, []), @@ -171,13 +192,75 @@ final_parse( Options = #{progname => Progname}, argparse:parse(Args, ArgparseDef, Options). -run_command(#{connection := Connection} = Context) -> - rabbit_cli_transport:run_command(Connection, Context); -run_command(Context) -> - rabbit_cli_commands:run_command(Context). +%% ------------------------------------------------------------------- +%% Configuation file. +%% ------------------------------------------------------------------- -noop(_Context) -> - ok. +read_config_file(_Context) -> + ConfigFilename = get_config_filename(), + case filelib:is_regular(ConfigFilename) of + true -> + SchemaFilename = get_config_schema_filename(), + Schema = cuttlefish_schema:files([SchemaFilename]), + case cuttlefish_conf:files([ConfigFilename]) of + {errorlist, Errors} -> + io:format(standard_error, "Errors1 = ~p~n", [Errors]), + {error, config}; + Config0 -> + case cuttlefish_generator:map(Schema, Config0) of + {error, _Phase, {errorlist, Errors}} -> + io:format( + standard_error, "Errors2 = ~p~n", [Errors]), + {error, config}; + Config1 -> + Config2 = proplists:get_value( + rabbitmqctl, Config1, []), + Config3 = maps:from_list(Config2), + {ok, Config3} + end + end; + false -> + {ok, #{}} + end. + +get_config_schema_filename() -> + ok = application:load(rabbit), + RabbitPrivDir = code:priv_dir(rabbit), + RabbitmqctlSchema = filename:join( + [RabbitPrivDir, "schema", "rabbitmqctl.schema"]), + RabbitmqctlSchema. + +get_config_filename() -> + {OsFamily, _} = os:type(), + get_config_filename(OsFamily). + +get_config_filename(unix) -> + XdgConfigHome = case os:getenv("XDG_CONFIG_HOME") of + false -> + HomeDir = os:getenv("HOME"), + ?assertNotEqual(false, HomeDir), + filename:join([HomeDir, ".config"]); + Value -> + Value + end, + ConfigFilename = filename:join( + [XdgConfigHome, "rabbitmq", "rabbitmqctl.conf"]), + ConfigFilename. + +%% ------------------------------------------------------------------- +%% Aliases handling. +%% ------------------------------------------------------------------- + +translate_aliases(Aliases) -> + Aliases1 = maps:from_list(Aliases), + Aliases2 = maps:map( + fun(_Alias, CommandStr) -> + Args = string:lexemes(CommandStr, " "), + #{alias => Args, + help => hidden, + handler => {?MODULE, handle_alias}} + end, Aliases1), + Aliases2. handle_alias( #{progname := Progname, @@ -195,3 +278,15 @@ handle_alias( {error, _} = Error -> Error end. + +%% ------------------------------------------------------------------- +%% Command execution. +%% ------------------------------------------------------------------- + +run_command(#{connection := Connection} = Context) -> + rabbit_cli_transport:run_command(Connection, Context); +run_command(Context) -> + rabbit_cli_commands:run_command(Context). + +noop(_Context) -> + ok. diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 51581eb3882f..0a87c035552b 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -99,16 +99,32 @@ expand_argparse_def(Defs) when is_list(Defs) -> run_command(Context) -> %% TODO: Put both processes under the rabbit supervision tree. - RunnerPid = spawn_link(fun() -> do_run_command(Context) end), + Parent = self(), + RunnerPid = spawn_link( + fun() -> + Ret = do_run_command(Context), + case Ret of + ok -> + ok; + {ok, _} -> + ok; + Other -> + erlang:unlink(Parent), + erlang:error(Other) + end + end), RunnerMRef = erlang:monitor(process, RunnerPid), receive + {'DOWN', RunnerMRef, _, _, normal} -> + ok; {'DOWN', RunnerMRef, _, _, Reason} -> - {ok, Reason} + Reason end. do_run_command(#{command := Command, arg_map := #{help := true}} = Context) when not is_map_key(alias, Command) -> - rabbit_cli_io:display_help(Context); + rabbit_cli_io:display_help(Context), + ok; do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> erlang:apply(Mod, Fun, [Context]). @@ -145,7 +161,8 @@ cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> Record2 = [Value || {_Key, Value} <- Record1], rabbit_cli_io:push_new_record(IO, Stream, Record2) end, Exchanges), - rabbit_cli_io:end_record_stream(IO, Stream); + rabbit_cli_io:end_record_stream(IO, Stream), + ok; {error, _} = Error -> Error end. From 90f2944ce559052fe322d98fe9ed2b2182f0cb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 2 Jan 2025 16:30:58 +0100 Subject: [PATCH 08/51] Add JSON formatting --- deps/rabbit/src/rabbit_cli_commands.erl | 21 ++-- deps/rabbit/src/rabbit_cli_io.erl | 142 +++++++++++++++++++----- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 0a87c035552b..2256a69c5e0d 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -3,6 +3,7 @@ -include_lib("kernel/include/logger.hrl"). -include_lib("rabbit_common/include/logging.hrl"). +-include_lib("rabbit_common/include/resource.hrl"). -export([argparse_def/0, run_command/1, do_run_command/1]). -export([cmd_list_exchanges/1]). @@ -89,10 +90,11 @@ expand_argparse_def(Def) when is_map(Def) -> Def; expand_argparse_def(Defs) when is_list(Defs) -> lists:foldl( - fun(argparse_def_record_stream, Acc) -> + fun + (argparse_def_record_stream, Acc) -> Def = rabbit_cli_io:argparse_def(record_stream), rabbit_cli:merge_argparse_def(Acc, Def); - (Def, Acc) -> + (Def, Acc) -> Def1 = expand_argparse_def(Def), rabbit_cli:merge_argparse_def(Acc, Def1) end, #{}, Defs). @@ -129,11 +131,11 @@ do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> erlang:apply(Mod, Fun, [Context]). cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> - InfoKeys = rabbit_exchange:info_keys(), + InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], Fields = lists:map( fun (name = Key) -> - #{name => Key, type => resource}; + #{name => Key, type => string}; (type = Key) -> #{name => Key, type => string}; (durable = Key) -> @@ -146,8 +148,6 @@ cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> #{name => Key, type => term}; (policy = Key) -> #{name => Key, type => string}; - (user_who_performed_action = Key) -> - #{name => Key, type => string}; (Key) -> #{name => Key, type => term} end, InfoKeys), @@ -156,9 +156,14 @@ cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> Exchanges = rabbit_exchange:list(), lists:foreach( fun(Exchange) -> - Record0 = rabbit_exchange:info(Exchange), + Record0 = rabbit_exchange:info(Exchange, InfoKeys), Record1 = lists:sublist(Record0, length(Fields)), - Record2 = [Value || {_Key, Value} <- Record1], + Record2 = [case Value of + #resource{name = N} -> + N; + _ -> + Value + end || {_Key, Value} <- Record1], rabbit_cli_io:push_new_record(IO, Stream, Record2) end, Exchanges), rabbit_cli_io:end_record_stream(IO, Stream), diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 8e4f21d7251d..ab71da249158 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -45,7 +45,7 @@ argparse_def(record_stream) -> long => "-format", short => $f, type => {atom, [plain, json]}, - nargs => 1, + default => plain, help => "Format output acccording to "} ] }. @@ -80,20 +80,17 @@ init(#{progname := Progname}) -> {ok, State}. handle_call( - {start_record_stream, Name, Fields, _ArgMap}, + {start_record_stream, Name, Fields, ArgMap}, From, #?MODULE{record_streams = Streams} = State) -> - Stream = #{name => Name, fields => Fields}, + Stream = #{name => Name, fields => Fields, arg_map => ArgMap}, + Streams1 = Streams#{Name => Stream}, + State1 = State#?MODULE{record_streams = Streams1}, gen_server:reply(From, {ok, Stream}), - FieldNames = [atom_to_list(FieldName) - || #{name := FieldName} <- Fields], - Header = string:join(FieldNames, "\t"), - io:format("~ts~n", [Header]), + {ok, State2} = format_record_stream_start(Name, State1), - Streams1 = Streams#{Name => Stream}, - State1 = State#?MODULE{record_streams = Streams1}, - {noreply, State1}; + {noreply, State2}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> @@ -109,16 +106,12 @@ handle_cast( Help = argparse:help(ArgparseDef, Options), io:format("~s~n", [Help]), {noreply, State}; -handle_cast( - {push_new_record, Name, Record}, - #?MODULE{record_streams = Streams} = State) -> - #{fields := Fields} = maps:get(Name, Streams), - Values = format_fields(Fields, Record), - Line = string:join(Values, "\t"), - io:format("~ts~n", [Line]), - {noreply, State}; -handle_cast({end_record_stream, _Name}, State) -> - {noreply, State}; +handle_cast({push_new_record, Name, Record}, State) -> + {ok, State1} = format_record(Name, Record, State), + {noreply, State1}; +handle_cast({end_record_stream, Name}, State) -> + {ok, State1} = format_record_stream_end(Name, State), + {noreply, State1}; handle_cast(_Request, State) -> {noreply, State}. @@ -131,6 +124,97 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +format_record_stream_start( + Name, + #?MODULE{record_streams = Streams} = State) -> + Stream = maps:get(Name, Streams), + format_record_stream_start1(Stream, State). + +format_record_stream_start1( + #{name := Name, fields := Fields, arg_map := #{format := plain}} = Stream, + #?MODULE{record_streams = Streams} = State) -> + FieldNames = [atom_to_list(FieldName) || #{name := FieldName} <- Fields], + FieldWidths = [case Field of + #{type := string, name := FieldName} -> + lists:max([length(atom_to_list(FieldName)), 20]); + #{name := FieldName} -> + length(atom_to_list(FieldName)) + end || Field <- Fields], + Format0 = [rabbit_misc:format("~~-~b.. ts", [Width]) + || Width <- FieldWidths], + Format1 = string:join(Format0, " "), + case isatty(standard_io) of + true -> + io:format("\033[1m" ++ Format1 ++ "\033[0m~n", FieldNames); + false -> + io:format(Format1 ++ "~n", FieldNames) + end, + Stream1 = Stream#{format => Format1}, + Streams1 = Streams#{Name => Stream1}, + State1 = State#?MODULE{record_streams = Streams1}, + {ok, State1}; +format_record_stream_start1( + #{name := Name, arg_map := #{format := json}} = Stream, + #?MODULE{record_streams = Streams} = State) -> + Stream1 = Stream#{emitted_fields => 0}, + Streams1 = Streams#{Name => Stream1}, + State1 = State#?MODULE{record_streams = Streams1}, + {ok, State1}. + +format_record(Name, Record, #?MODULE{record_streams = Streams} = State) -> + Stream = maps:get(Name, Streams), + format_record1(Stream, Record, State). + +format_record1( + #{fields := Fields, arg_map := #{format := plain}, + format := Format}, + Record, + State) -> + Values = format_fields(Fields, Record), + io:format(Format ++ "~n", Values), + {ok, State}; +format_record1( + #{fields := Fields, arg_map := #{format := json}, + name := Name, emitted_fields := Emitted} = Stream, + Record, + #?MODULE{record_streams = Streams} = State) -> + Fields1 = [FieldName || #{name := FieldName} <- Fields], + Struct = lists:zip(Fields1, Record), + Json = json:encode( + Struct, + fun + ([{_, _} | _] = Value, Encode) -> + json:encode_key_value_list(Value, Encode); + (Value, Encode) -> + json:encode_value(Value, Encode) + end), + case Emitted of + 0 -> + io:format("[~n ~ts", [Json]); + _ -> + io:format(",~n ~ts", [Json]) + end, + Stream1 = Stream#{emitted_fields => Emitted + 1}, + Streams1 = Streams#{Name => Stream1}, + State1 = State#?MODULE{record_streams = Streams1}, + {ok, State1}. + +format_record_stream_end( + Name, + #?MODULE{record_streams = Streams} = State) -> + Stream = maps:get(Name, Streams), + {ok, State1} = format_record_stream_end1(Stream, State), + #?MODULE{record_streams = Streams1} = State1, + Streams2 = maps:remove(Name, Streams1), + State2 = State1#?MODULE{record_streams = Streams2}, + {ok, State2}. + +format_record_stream_end1(#{arg_map := #{format := plain}}, State) -> + {ok, State}; +format_record_stream_end1(#{arg_map := #{format := json}}, State) -> + io:format("~n]~n", []), + {ok, State}. + format_fields(Fields, Values) -> format_fields(Fields, Values, []). @@ -138,6 +222,10 @@ format_fields([#{type := string} | Rest1], [Value | Rest2], Acc) -> String = io_lib:format("~ts", [Value]), Acc1 = [String | Acc], format_fields(Rest1, Rest2, Acc1); +format_fields([#{type := binary} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~-20.. ts", [Value]), + Acc1 = [String | Acc], + format_fields(Rest1, Rest2, Acc1); format_fields([#{type := integer} | Rest1], [Value | Rest2], Acc) -> String = io_lib:format("~b", [Value]), Acc1 = [String | Acc], @@ -146,14 +234,18 @@ format_fields([#{type := boolean} | Rest1], [Value | Rest2], Acc) -> String = io_lib:format("~ts", [if Value -> "☑"; true -> "☐" end]), Acc1 = [String | Acc], format_fields(Rest1, Rest2, Acc1); -format_fields([#{type := resource} | Rest1], [Value | Rest2], Acc) -> - #resource{name = Name} = Value, - String = io_lib:format("~ts", [Name]), - Acc1 = [String | Acc], - format_fields(Rest1, Rest2, Acc1); format_fields([#{type := term} | Rest1], [Value | Rest2], Acc) -> String = io_lib:format("~0p", [Value]), Acc1 = [String | Acc], format_fields(Rest1, Rest2, Acc1); format_fields([], [], Acc) -> lists:reverse(Acc). + +isatty(IoDevice) -> + Opts = io:getopts(IoDevice), + case proplists:get_value(stdout, Opts) of + true -> + true; + _ -> + false + end. From da4c4956675d750e7b9c91a9beeb2111f7150d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 9 Apr 2025 15:27:03 +0200 Subject: [PATCH 09/51] Drop alias support --- deps/rabbit/priv/schema/rabbitmqctl.schema | 11 ----- deps/rabbit/src/rabbit_cli.erl | 49 +--------------------- deps/rabbit/src/rabbit_cli_commands.erl | 4 -- 3 files changed, 2 insertions(+), 62 deletions(-) diff --git a/deps/rabbit/priv/schema/rabbitmqctl.schema b/deps/rabbit/priv/schema/rabbitmqctl.schema index 2319fbecdaa4..1d4f6143f7a0 100644 --- a/deps/rabbit/priv/schema/rabbitmqctl.schema +++ b/deps/rabbit/priv/schema/rabbitmqctl.schema @@ -1,12 +1 @@ %% vim:ft=erlang:sw=4:et: - -{mapping, "alias.$alias", "rabbitmqctl.aliases", - [{datatype, string}]}. - -{translation, "rabbitmqctl.aliases", - fun(Conf) -> - Aliases0 = cuttlefish_variable:filter_by_prefix("alias", Conf), - Aliases1 = [{Alias, Value} || {["alias", Alias], Value} <- Aliases0], - Aliases2 = rabbit_cli:translate_aliases(Aliases1), - Aliases2 - end}. diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index ed67033a34db..38f8b59c1b33 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -5,8 +5,6 @@ -export([main/1, merge_argparse_def/2, - translate_aliases/1, - handle_alias/1, noop/1]). main(Args) -> @@ -146,20 +144,9 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> get_final_argparse_def(#{argparse_def := PartialArgparseDef} = Context) -> maybe - {ok, Aliases} ?= get_aliases(Context), {ok, FullArgparseDef} ?= get_full_argparse_def(Context), - ArgparseDef1 = merge_argparse_def(PartialArgparseDef, Aliases), - ArgparseDef2 = merge_argparse_def(ArgparseDef1, FullArgparseDef), - {ok, ArgparseDef2} - end. - -get_aliases(#{config := Config}) -> - Aliases = maps:get(aliases, Config, #{}), - case Aliases =:= #{} of - true -> - {ok, #{}}; - false -> - {ok, #{commands => Aliases}} + ArgparseDef1 = merge_argparse_def(PartialArgparseDef, FullArgparseDef), + {ok, ArgparseDef1} end. get_full_argparse_def(#{connection := Connection}) -> @@ -247,38 +234,6 @@ get_config_filename(unix) -> [XdgConfigHome, "rabbitmq", "rabbitmqctl.conf"]), ConfigFilename. -%% ------------------------------------------------------------------- -%% Aliases handling. -%% ------------------------------------------------------------------- - -translate_aliases(Aliases) -> - Aliases1 = maps:from_list(Aliases), - Aliases2 = maps:map( - fun(_Alias, CommandStr) -> - Args = string:lexemes(CommandStr, " "), - #{alias => Args, - help => hidden, - handler => {?MODULE, handle_alias}} - end, Aliases1), - Aliases2. - -handle_alias( - #{progname := Progname, - argparse_def := ArgparseDef, - arg_map := ArgMap, - command := #{alias := Args}} = Context) -> - Options = #{progname => Progname}, - case argparse:parse(Args, ArgparseDef, Options) of - {ok, ArgMap1, CmdPath1, Command1} -> - ArgMap2 = maps:merge(ArgMap1, ArgMap), - Context1 = Context#{arg_map => ArgMap2, - cmd_path => CmdPath1, - command => Command1}, - rabbit_cli_commands:do_run_command(Context1); - {error, _} = Error -> - Error - end. - %% ------------------------------------------------------------------- %% Command execution. %% ------------------------------------------------------------------- diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 2256a69c5e0d..23962f587200 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -123,10 +123,6 @@ run_command(Context) -> Reason end. -do_run_command(#{command := Command, arg_map := #{help := true}} = Context) - when not is_map_key(alias, Command) -> - rabbit_cli_io:display_help(Context), - ok; do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> erlang:apply(Mod, Fun, [Context]). From 628145579eb5476c508d03d37b0c417c7299bd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 3 Jan 2025 13:49:39 +0100 Subject: [PATCH 10/51] Read input file support [skip ci] --- deps/rabbit/src/rabbit_cli_commands.erl | 21 +++++++++- deps/rabbit/src/rabbit_cli_io.erl | 54 ++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 23962f587200..e144d9290167 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -6,7 +6,8 @@ -include_lib("rabbit_common/include/resource.hrl"). -export([argparse_def/0, run_command/1, do_run_command/1]). --export([cmd_list_exchanges/1]). +-export([cmd_list_exchanges/1, + cmd_import_definitions/1]). -rabbitmq_command( {#{cli => ["declare", "exchange"], @@ -31,6 +32,12 @@ #{help => "List exchanges", handler => {?MODULE, cmd_list_exchanges}}]}). +-rabbitmq_command( + {#{cli => ["import", "definitions"]}, + [argparse_def_file_input, + #{help => "Import definitions", + handler => {?MODULE, cmd_import_definitions}}]}). + argparse_def() -> #{argparse_def := ArgparseDef} = get_discovered_commands(), ArgparseDef. @@ -94,6 +101,9 @@ expand_argparse_def(Defs) when is_list(Defs) -> (argparse_def_record_stream, Acc) -> Def = rabbit_cli_io:argparse_def(record_stream), rabbit_cli:merge_argparse_def(Acc, Def); + (argparse_def_file_input, Acc) -> + Def = rabbit_cli_io:argparse_def(file_input), + rabbit_cli:merge_argparse_def(Acc, Def); (Def, Acc) -> Def1 = expand_argparse_def(Def), rabbit_cli:merge_argparse_def(Acc, Def1) @@ -167,3 +177,12 @@ cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> {error, _} = Error -> Error end. + +cmd_import_definitions(#{arg_map := ArgMap, io := IO}) -> + case rabbit_cli_io:read_file(IO, ArgMap) of + {ok, Data} -> + rabbit_cli_io:format(IO, "Import definitions:~n ~s~n", [Data]), + ok; + {error, _} = Error -> + Error + end. diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index ab71da249158..d890e26bcda5 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -8,9 +8,11 @@ stop/1, argparse_def/1, display_help/1, + format/3, start_record_stream/4, push_new_record/3, - end_record_stream/2]). + end_record_stream/2, + read_file/2]). -export([init/1, handle_call/3, handle_cast/2, @@ -48,6 +50,17 @@ argparse_def(record_stream) -> default => plain, help => "Format output acccording to "} ] + }; +argparse_def(file_input) -> + #{arguments => + [ + #{name => input, + long => "-input", + short => $i, + type => string, + nargs => 1, + help => "Read input from file "} + ] }. display_help(#{io := {transport, Transport}} = Context) -> @@ -55,6 +68,11 @@ display_help(#{io := {transport, Transport}} = Context) -> display_help(#{io := IO} = Context) -> gen_server:cast(IO, {?FUNCTION_NAME, Context}). +format({transport, Transport}, Format, Args) -> + Transport ! {io_cast, {?FUNCTION_NAME, Format, Args}}; +format(IO, Format, Args) -> + gen_server:cast(IO, {?FUNCTION_NAME, Format, Args}). + start_record_stream({transport, Transport}, Name, Fields, ArgMap) -> Transport ! {io_call, self(), {?FUNCTION_NAME, Name, Fields, ArgMap}}, receive Ret -> Ret end; @@ -74,6 +92,14 @@ end_record_stream({transport, Transport}, #{name := Name}) -> end_record_stream(IO, #{name := Name}) -> gen_server:cast(IO, {?FUNCTION_NAME, Name}). +read_file({transport, Transport}, ArgMap) -> + Transport ! {io_call, self(), {?FUNCTION_NAME, ArgMap}}, + receive Ret -> Ret end; +read_file(IO, ArgMap) + when is_pid(IO) andalso + is_map(ArgMap) -> + gen_server:call(IO, {?FUNCTION_NAME, ArgMap}). + init(#{progname := Progname}) -> process_flag(trap_exit, true), State = #?MODULE{progname = Progname}, @@ -91,6 +117,9 @@ handle_call( {ok, State2} = format_record_stream_start(Name, State1), {noreply, State2}; +handle_call({read_file, ArgMap}, From, State) -> + {ok, State1} = do_read_file(ArgMap, From, State), + {noreply, State1}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> @@ -106,6 +135,9 @@ handle_cast( Help = argparse:help(ArgparseDef, Options), io:format("~s~n", [Help]), {noreply, State}; +handle_cast({format, Format, Args}, State) -> + io:format(Format, Args), + {noreply, State}; handle_cast({push_new_record, Name, Record}, State) -> {ok, State1} = format_record(Name, Record, State), {noreply, State1}; @@ -249,3 +281,23 @@ isatty(IoDevice) -> _ -> false end. + +do_read_file(#{input := "-"}, From, State) -> + Ret = read_stdin(<<>>), + gen:reply(From, Ret), + {ok, State}; +do_read_file(#{input := Filename}, From, State) -> + Ret = file:read_file(Filename), + gen:reply(From, Ret), + {ok, State}. + +read_stdin(Buf) -> + case file:read(standard_io, 4096) of + {ok, Data} -> + Buf1 = [Buf, Data], + read_stdin(Buf1); + eof -> + {ok, Buf}; + {error, _} = Error -> + Error + end. From c623487fe230208a8710d5a18a29b3098943aba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 23 Apr 2025 10:25:42 +0200 Subject: [PATCH 11/51] Test interactive UI --- deps/rabbit/src/rabbit_cli_commands.erl | 29 +++++++++++- deps/rabbit/src/rabbit_cli_curses.erl | 6 +++ deps/rabbit/src/rabbit_cli_io.erl | 61 +++++++++++++++++++++---- 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 deps/rabbit/src/rabbit_cli_curses.erl diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index e144d9290167..4137aaef98b1 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -7,7 +7,8 @@ -export([argparse_def/0, run_command/1, do_run_command/1]). -export([cmd_list_exchanges/1, - cmd_import_definitions/1]). + cmd_import_definitions/1, + cmd_top/1]). -rabbitmq_command( {#{cli => ["declare", "exchange"], @@ -38,6 +39,11 @@ #{help => "Import definitions", handler => {?MODULE, cmd_import_definitions}}]}). +-rabbitmq_command( + {#{cli => ["top"]}, + [#{help => "Top-like interactive view", + handler => {?MODULE, cmd_top}}]}). + argparse_def() -> #{argparse_def := ArgparseDef} = get_discovered_commands(), ArgparseDef. @@ -186,3 +192,24 @@ cmd_import_definitions(#{arg_map := ArgMap, io := IO}) -> {error, _} = Error -> Error end. + +cmd_top(#{io := IO} = Context) -> + Top = spawn_link(fun() -> run_top(IO) end), + wait_quit(Context, Top). + +run_top(IO) -> + receive + quit -> + ok + after 1000 -> + rabbit_cli_io:format(IO, "Refresh~n", []), + run_top(IO) + end. + +wait_quit(#{arg_map := _ArgMap, io := _IO}, Top) -> + receive + {keypress, _} -> + erlang:unlink(Top), + Top ! quit, + ok + end. diff --git a/deps/rabbit/src/rabbit_cli_curses.erl b/deps/rabbit/src/rabbit_cli_curses.erl new file mode 100644 index 000000000000..55161868b35b --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_curses.erl @@ -0,0 +1,6 @@ +-module(rabbit_cli_curses). + +-export([init/0]). + +init() -> + window. diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index d890e26bcda5..4104963f78f3 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -12,6 +12,7 @@ start_record_stream/4, push_new_record/3, end_record_stream/2, + send_keyboard_input/3, read_file/2]). -export([init/1, handle_call/3, @@ -21,7 +22,9 @@ code_change/3]). -record(?MODULE, {progname, - record_streams = #{}}). + record_streams = #{}, + kbd_reader = undefined, + kbd_subscribers = []}). start_link(Progname) -> gen_server:start_link(rabbit_cli_io, #{progname => Progname}, []). @@ -92,6 +95,14 @@ end_record_stream({transport, Transport}, #{name := Name}) -> end_record_stream(IO, #{name := Name}) -> gen_server:cast(IO, {?FUNCTION_NAME, Name}). +send_keyboard_input({transport, Transport}, ArgMap, Subscriber) -> + Transport ! {io_call, self(), {?FUNCTION_NAME, ArgMap, Subscriber}}, + receive Ret -> Ret end; +send_keyboard_input(IO, ArgMap, Subscriber) + when is_pid(IO) andalso + is_map(ArgMap) -> + gen_server:call(IO, {?FUNCTION_NAME, ArgMap, Subscriber}). + read_file({transport, Transport}, ArgMap) -> Transport ! {io_call, self(), {?FUNCTION_NAME, ArgMap}}, receive Ret -> Ret end; @@ -116,14 +127,21 @@ handle_call( {ok, State2} = format_record_stream_start(Name, State1), - {noreply, State2}; + {noreply, State2, compute_timeout(State2)}; +handle_call( + {send_keyboard_input, _ArgMap, Subscriber}, + _From, + #?MODULE{kbd_subscribers = Subscribers} = State) -> + Subscribers1 = [Subscriber | Subscribers], + State1 = State#?MODULE{kbd_subscribers = Subscribers1}, + {reply, ok, State1, compute_timeout(State1)}; handle_call({read_file, ArgMap}, From, State) -> {ok, State1} = do_read_file(ArgMap, From, State), - {noreply, State1}; + {noreply, State1, compute_timeout(State1)}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> - {reply, ok, State}. + {reply, ok, State, compute_timeout(State)}. handle_cast( {display_help, #{cmd_path := CmdPath, argparse_def := ArgparseDef}}, @@ -134,21 +152,39 @@ handle_cast( command => tl(CmdPath)}, Help = argparse:help(ArgparseDef, Options), io:format("~s~n", [Help]), - {noreply, State}; + {noreply, State, compute_timeout(State)}; handle_cast({format, Format, Args}, State) -> io:format(Format, Args), - {noreply, State}; + {noreply, State, compute_timeout(State)}; handle_cast({push_new_record, Name, Record}, State) -> {ok, State1} = format_record(Name, Record, State), - {noreply, State1}; + {noreply, State1, compute_timeout(State1)}; handle_cast({end_record_stream, Name}, State) -> {ok, State1} = format_record_stream_end(Name, State), - {noreply, State1}; + {noreply, State1, compute_timeout(State1)}; handle_cast(_Request, State) -> - {noreply, State}. + {noreply, State, compute_timeout(State)}. +handle_info(timeout, #?MODULE{kbd_reader = Reader} = State) + when is_pid(Reader) -> + {noreply, State}; +handle_info(timeout, #?MODULE{kbd_subscribers = []} = State) -> + {noreply, State}; +handle_info(timeout, #?MODULE{kbd_subscribers = Subscribers} = State) -> + Parent = self(), + Reader = spawn_link( + fun() -> + Ret = io:read(""), + lists:foreach( + fun(Sub) -> + Sub ! {keypress, Ret} + end, Subscribers), + erlang:unlink(Parent) + end, Subscribers), + State1 = State#?MODULE{kbd_reader = Reader}, + {noreply, State1, compute_timeout(State1)}; handle_info(_Info, State) -> - {noreply, State}. + {noreply, State, compute_timeout(State)}. terminate(_Reason, _State) -> ok. @@ -156,6 +192,11 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +compute_timeout(#?MODULE{kbd_subscribers = []}) -> + infinity; +compute_timeout(#?MODULE{kbd_subscribers = _}) -> + 0. + format_record_stream_start( Name, #?MODULE{record_streams = Streams} = State) -> From a764c2c04c24daa3faae4a5177a4a69918abab04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 25 Apr 2025 15:13:06 +0200 Subject: [PATCH 12/51] Use Erlang I/O protocol, not ours --- deps/rabbit/src/rabbit_cli.erl | 15 ++-- deps/rabbit/src/rabbit_cli_commands.erl | 68 ++++++++++-------- deps/rabbit/src/rabbit_cli_transport.erl | 89 ++++++++++++++---------- deps/rabbit/src/rabbit_cli_ws.erl | 31 ++++++--- deps/rabbit/src/rabbit_cli_ws_runner.erl | 32 +++------ 5 files changed, 127 insertions(+), 108 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl index 38f8b59c1b33..f79e2f43db29 100644 --- a/deps/rabbit/src/rabbit_cli.erl +++ b/deps/rabbit/src/rabbit_cli.erl @@ -17,20 +17,14 @@ run_cli(Args) -> Progname = escript:script_name(), ok ?= add_rabbitmq_code_path(Progname), - {ok, IO} ?= rabbit_cli_io:start_link(Progname), - - try - do_run_cli(Progname, Args, IO) - after - rabbit_cli_io:stop(IO) - end + do_run_cli(Progname, Args) end. -do_run_cli(Progname, Args, IO) -> +do_run_cli(Progname, Args) -> PartialArgparseDef = argparse_def(), Context0 = #{progname => Progname, args => Args, - io => IO, + group_leader => erlang:group_leader(), argparse_def => PartialArgparseDef}, maybe {ok, @@ -239,7 +233,8 @@ get_config_filename(unix) -> %% ------------------------------------------------------------------- run_command(#{connection := Connection} = Context) -> - rabbit_cli_transport:run_command(Connection, Context); + rabbit_cli_transport:rpc( + Connection, rabbit_cli_commands, run_command, [Context]); run_command(Context) -> rabbit_cli_commands:run_command(Context). diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 4137aaef98b1..51ae6973df6d 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -142,7 +142,8 @@ run_command(Context) -> do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> erlang:apply(Mod, Fun, [Context]). -cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> +cmd_list_exchanges(#{progname := Progname, arg_map := ArgMap}) -> + logger:alert("CLI: running list exchanges"), InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], Fields = lists:map( fun @@ -163,35 +164,42 @@ cmd_list_exchanges(#{arg_map := ArgMap, io := IO}) -> (Key) -> #{name => Key, type => term} end, InfoKeys), - case rabbit_cli_io:start_record_stream(IO, exchanges, Fields, ArgMap) of - {ok, Stream} -> - Exchanges = rabbit_exchange:list(), - lists:foreach( - fun(Exchange) -> - Record0 = rabbit_exchange:info(Exchange, InfoKeys), - Record1 = lists:sublist(Record0, length(Fields)), - Record2 = [case Value of - #resource{name = N} -> - N; - _ -> - Value - end || {_Key, Value} <- Record1], - rabbit_cli_io:push_new_record(IO, Stream, Record2) - end, Exchanges), - rabbit_cli_io:end_record_stream(IO, Stream), - ok; - {error, _} = Error -> - Error - end. - -cmd_import_definitions(#{arg_map := ArgMap, io := IO}) -> - case rabbit_cli_io:read_file(IO, ArgMap) of - {ok, Data} -> - rabbit_cli_io:format(IO, "Import definitions:~n ~s~n", [Data]), - ok; - {error, _} = Error -> - Error - end. + {ok, IO} = rabbit_cli_io:start_link(Progname), + Ret = case rabbit_cli_io:start_record_stream(IO, exchanges, Fields, ArgMap) of + {ok, Stream} -> + Exchanges = rabbit_exchange:list(), + lists:foreach( + fun(Exchange) -> + Record0 = rabbit_exchange:info(Exchange, InfoKeys), + Record1 = lists:sublist(Record0, length(Fields)), + Record2 = [case Value of + #resource{name = N} -> + N; + _ -> + Value + end || {_Key, Value} <- Record1], + rabbit_cli_io:push_new_record(IO, Stream, Record2) + end, Exchanges), + rabbit_cli_io:end_record_stream(IO, Stream), + ok; + {error, _} = Error -> + Error + end, + rabbit_cli_io:stop(IO), + Ret. + +cmd_import_definitions(#{progname := Progname, arg_map := ArgMap}) -> + {ok, IO} = rabbit_cli_io:start_link(Progname), + %% TODO: Use a wrapper above `file' to proxy through transport. + Ret = case rabbit_cli_io:read_file(IO, ArgMap) of + {ok, Data} -> + rabbit_cli_io:format(IO, "Import definitions:~n ~s~n", [Data]), + ok; + {error, _} = Error -> + Error + end, + rabbit_cli_io:stop(IO), + Ret. cmd_top(#{io := IO} = Context) -> Top = spawn_link(fun() -> run_top(IO) end), diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl index d10e24f0aac7..552fd2472283 100644 --- a/deps/rabbit/src/rabbit_cli_transport.erl +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -2,8 +2,7 @@ -behaviour(gen_server). -export([connect/1, - rpc/4, - run_command/2]). + rpc/4]). -export([init/1, handle_call/3, handle_cast/2, @@ -16,7 +15,8 @@ stream :: gun:stream_ref(), stream_ready = false :: boolean(), pending = [] :: [any()], - io :: pid() + pending_io_requests = #{} :: map(), + group_leader :: pid() }). connect(#{arg_map := #{node := NodenameOrUri}} = Context) -> @@ -34,11 +34,6 @@ rpc(Nodename, Mod, Func, Args) when is_atom(Nodename) -> rpc(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> rpc_using_transport(TransportPid, Mod, Func, Args). -run_command(Nodename, Context) when is_atom(Nodename) -> - run_command_using_erldist(Nodename, Context); -run_command(TransportPid, Context) when is_pid(TransportPid) -> - run_command_using_transport(TransportPid, Context). - %% ------------------------------------------------------------------- %% Erlang distribution. %% ------------------------------------------------------------------- @@ -96,9 +91,6 @@ complete_nodename(Nodename) -> rpc_using_erldist(Nodename, Mod, Func, Args) -> erpc:call(Nodename, Mod, Func, Args). -run_command_using_erldist(Nodename, Context) -> - erpc:call(Nodename, rabbit_cli_commands, run_command, [Context]). - %% ------------------------------------------------------------------- %% HTTP(S) transport. %% ------------------------------------------------------------------- @@ -109,17 +101,14 @@ connect_using_transport(Context) -> rpc_using_transport(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> gen_server:call(TransportPid, {rpc, {Mod, Func, Args}}). -run_command_using_transport(TransportPid, Context) when is_pid(TransportPid) -> - gen_server:call(TransportPid, {run_command, Context}). - -init(#{arg_map := #{node := Uri}, io := IO}) -> +init(#{arg_map := #{node := Uri}, group_leader := GL}) -> maybe {ok, _} ?= application:ensure_all_started(gun), #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), {ok, ConnPid} ?= gun:open(Host, Port), State = #http{uri = UriMap, - conn = ConnPid, - io = IO}, + group_leader = GL, + conn = ConnPid}, %logger:alert("Transport: State=~p", [State]), {ok, State} end. @@ -127,6 +116,7 @@ init(#{arg_map := #{node := Uri}, io := IO}) -> handle_call( Request, From, #http{stream_ready = true} = State) -> + %% HTTP message to the server side. send_call(Request, From, State), {noreply, State}; handle_call( @@ -163,26 +153,53 @@ handle_info( {noreply, State1}; handle_info( {gun_ws, ConnPid, StreamRef, {binary, ReplyBin}}, - #http{conn = ConnPid, stream = StreamRef, io = IO} = State) -> + #http{conn = ConnPid, + stream = StreamRef, + group_leader = GL, + pending_io_requests = Pending} = State) -> + %% HTTP message from the server side. Reply = binary_to_term(ReplyBin), - case Reply of - {io_call, From, Msg} -> - %logger:alert("IO call from WS: ~p -> ~p", [Msg, From]), - Ret = gen_server:call(IO, Msg), - RequestBin = term_to_binary({io_reply, From, Ret}), - Frame = {binary, RequestBin}, - gun:ws_send(ConnPid, StreamRef, Frame); - {io_cast, Msg} -> - %logger:alert("IO cast from WS: ~p", [Msg]), - gen_server:cast(IO, Msg); - {ret, From, Ret} -> - %logger:alert("Reply from WS: ~p -> ~p", [Ret, From]), - gen_server:reply(From, Ret); - _Other -> - %logger:alert("Reply from WS: ~p", [_Other]), - ok - end, - {noreply, State}; + State1 = case Reply of + % {io_call, From, Msg} -> + % %logger:alert("IO call from WS: ~p -> ~p", [Msg, From]), + % Ret = gen_server:call(IO, Msg), + % RequestBin = term_to_binary({io_reply, From, Ret}), + % Frame = {binary, RequestBin}, + % gun:ws_send(ConnPid, StreamRef, Frame); + % {io_cast, Msg} -> + % %logger:alert("IO cast from WS: ~p", [Msg]), + % gen_server:cast(IO, Msg); + {msg, group_leader, {io_request, RemoteFrom, ReplyAs, Request} = _Msg} -> + % logger:alert("Message from WS: ~p", [Msg]), + Ref = erlang:make_ref(), + IoRequest1 = {io_request, self(), Ref, Request}, + GL ! IoRequest1, + Pending1 = Pending#{Ref => {RemoteFrom, ReplyAs}}, + State#http{pending_io_requests = Pending1}; + {ret, From, Ret} -> + %logger:alert("Reply from WS: ~p -> ~p", [Ret, From]), + gen_server:reply(From, Ret), + State; + _Other -> + %logger:alert("Reply from WS: ~p", [_Other]), + State + end, + {noreply, State1}; +handle_info( + {io_reply, ReplyAs, Reply} = _IoReply, + #http{conn = ConnPid, + stream = StreamRef, + pending_io_requests = Pending} = State) -> + % logger:alert("io_reply to WS: ~p", [IoReply]), + {RemoteFrom, RemoteReplyAs} = maps:get(ReplyAs, Pending), + Msg = {io_reply, RemoteReplyAs, Reply}, + RequestBin = term_to_binary({msg, RemoteFrom, Msg}), + Frame = {binary, RequestBin}, + gun:ws_send(ConnPid, StreamRef, Frame), + + Pending1 = maps:remove(ReplyAs, Pending), + State1 = State#http{pending_io_requests = Pending1}, + {noreply, State1}; handle_info(_Info, State) -> %logger:alert("Transport(info): ~p", [_Info]), {noreply, State}. diff --git a/deps/rabbit/src/rabbit_cli_ws.erl b/deps/rabbit/src/rabbit_cli_ws.erl index 44ee7e27cc5b..e103bc60d1a5 100644 --- a/deps/rabbit/src/rabbit_cli_ws.erl +++ b/deps/rabbit/src/rabbit_cli_ws.erl @@ -51,20 +51,24 @@ init(Req, State) -> {cowboy_websocket, Req, State, #{idle_timeout => 30000}}. websocket_init(State) -> - {ok, Runner} = rabbit_cli_ws_runner:start_link( - self(), {transport, self()}), + {ok, Runner} = rabbit_cli_ws_runner:start_link(self()), State1 = State#{runner => Runner}, {ok, State1}. websocket_handle({binary, RequestBin}, State) -> + %% HTTP message from the client side. Request = binary_to_term(RequestBin), case Request of - {io_reply, From, Ret} -> - From ! Ret, - {ok, State}; + % {io_reply, From, Ret} -> + % From ! Ret, + % {ok, State}; {call, From, Call} -> handle_ws_call(Call, From, State), {ok, State}; + {msg, To, Msg} -> + % logger:alert("Message to ~0p: ~p", [To, Msg]), + To ! Msg, + {ok, State}; _ -> logger:alert("Unknown request: ~p", [Request]), ReplyBin = term_to_binary({error, Request}), @@ -75,12 +79,17 @@ websocket_handle(_Frame, State) -> logger:alert("Frame: ~p", [_Frame]), {ok, State}. -websocket_info({io_call, _From, _Msg} = Call, State) -> - ReplyBin = term_to_binary(Call), - Frame = {binary, ReplyBin}, - {[Frame], State}; -websocket_info({io_cast, _Msg} = Call, State) -> - ReplyBin = term_to_binary(Call), +% websocket_info({io_call, _From, _Msg} = Call, State) -> +% ReplyBin = term_to_binary(Call), +% Frame = {binary, ReplyBin}, +% {[Frame], State}; +% websocket_info({io_cast, _Msg} = Call, State) -> +% ReplyBin = term_to_binary(Call), +% Frame = {binary, ReplyBin}, +% {[Frame], State}; +websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> + % logger:alert("WS/cowboy: ~p", [IoRequest]), + ReplyBin = term_to_binary({msg, group_leader, IoRequest}), Frame = {binary, ReplyBin}, {[Frame], State}; websocket_info({reply, Ret, From}, State) -> diff --git a/deps/rabbit/src/rabbit_cli_ws_runner.erl b/deps/rabbit/src/rabbit_cli_ws_runner.erl index 5ca7364b1b5b..d5d38cc04711 100644 --- a/deps/rabbit/src/rabbit_cli_ws_runner.erl +++ b/deps/rabbit/src/rabbit_cli_ws_runner.erl @@ -1,7 +1,7 @@ -module(rabbit_cli_ws_runner). -behaviour(gen_server). --export([start_link/2, +-export([start_link/1, stop/1]). -export([init/1, handle_call/3, @@ -10,14 +10,21 @@ terminate/2, config_change/3]). -start_link(WS, IO) -> - gen_server:start_link(?MODULE, #{ws => WS, io => IO}, []). +start_link(WS) -> + gen_server:start_link(?MODULE, #{ws => WS}, []). stop(Runner) -> gen_server:stop(Runner). -init(#{ws := _, io := _} = Args) -> +init(#{ws := WS} = Args) -> process_flag(trap_exit, true), + _GL = erlang:group_leader(), + erlang:group_leader(WS, self()), + % spawn(fun() -> + % logger:alert("GL: ~0p -> ~0p", [GL, erlang:group_leader()]), + % io:format("GL: ~0p -> ~0p~n", [GL, erlang:group_leader()]), + % logger:alert("done with GL switch") + % end), {ok, Args}. handle_call(_Request, _From, State) -> @@ -38,29 +45,12 @@ handle_cast( WS ! {reply, Ex, From}, {noreply, State} end; -handle_cast( - {{run_command, Context}, From}, - #{ws := WS, io := IO} = State) -> - try - Context1 = Context#{io => IO}, - Ret = erlang:apply(rabbit_cli_commands, run_command, [Context1]), - logger:alert("Runner(rpc): ~p", [Ret]), - WS ! {reply, Ret, From}, - {noreply, State} - catch - Class:Reason:Stacktrace -> - Ex = {exception, Class, Reason, Stacktrace}, - WS ! {reply, Ex, From}, - {noreply, State} - end; handle_cast(_Request, State) -> logger:alert("Runner(cast): ~p", [_Request]), {noreply, State}. handle_info({'EXIT', WS, _Reason}, #{ws := WS} = State) -> {stop, State}; -handle_info({'EXIT', IO, _Reason}, #{io := IO} = State) -> - {stop, State}; handle_info(_Info, State) -> logger:alert("Runner/gen_server: ~p, ~p", [_Info, State]), {noreply, State}. From c471785bf529f61aebd9a749f394462f9a33643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 12 Jun 2025 18:53:15 +0200 Subject: [PATCH 13/51] Starting 2nd prototype --- deps/rabbit/Makefile | 4 +- deps/rabbit/priv/cli_http_help.html | 52 +++++ deps/rabbit/priv/schema/rabbit.schema | 8 + deps/rabbit/src/rabbit.erl | 8 +- deps/rabbit/src/rabbit_cli2.erl | 198 ++++++++++++++++++ deps/rabbit/src/rabbit_cli_http_client.erl | 186 +++++++++++++++++ deps/rabbit/src/rabbit_cli_http_listener.erl | 199 +++++++++++++++++++ deps/rabbit/src/rabbit_cli_http_server.erl | 81 ++++++++ deps/rabbit/src/rabbit_cli_transport2.erl | 86 ++++++++ 9 files changed, 819 insertions(+), 3 deletions(-) create mode 100644 deps/rabbit/priv/cli_http_help.html create mode 100644 deps/rabbit/src/rabbit_cli2.erl create mode 100644 deps/rabbit/src/rabbit_cli_http_client.erl create mode 100644 deps/rabbit/src/rabbit_cli_http_listener.erl create mode 100644 deps/rabbit/src/rabbit_cli_http_server.erl create mode 100644 deps/rabbit/src/rabbit_cli_transport2.erl diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index be293f8cf324..253d7b7e27fa 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -158,12 +158,12 @@ DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk include ../../rabbitmq-components.mk include ../../erlang.mk -ESCRIPT_NAME := rabbit_cli +ESCRIPT_NAME := rabbit_cli2 ESCRIPT_FILE := scripts/rmq ebin/$(PROJECT).app:: $(ESCRIPT_FILE) -$(ESCRIPT_FILE): ebin/rabbit_cli.beam +$(ESCRIPT_FILE): ebin/rabbit_cli2.beam $(gen_verbose) printf "%s\n" \ "#!$(ESCRIPT_SHEBANG)" \ "%% $(ESCRIPT_COMMENT)" \ diff --git a/deps/rabbit/priv/cli_http_help.html b/deps/rabbit/priv/cli_http_help.html new file mode 100644 index 000000000000..542f7dd83fa0 --- /dev/null +++ b/deps/rabbit/priv/cli_http_help.html @@ -0,0 +1,52 @@ + + + + + RabbitMQ CLI over HTTP + + + + +

RabbitMQ CLI over HTTP

+
+
+ + + +
+
+

This HTTP endpoint can be used by the RabbitMQ CLI — if this + URL is passed to it — instead of the default Erlang distribution + mechanism. To access this HTTP endpoint, the CLI will require + authentication using one of the configured RabbitMQ authentication + methods.

+

To manage this RabbitMQ node with the CLI over HTTP, run:

+
rabbitmqctl \
+    --node <URL> \
+    <command>
+
+
+ + diff --git a/deps/rabbit/priv/schema/rabbit.schema b/deps/rabbit/priv/schema/rabbit.schema index f5b79370fcd6..b89997349cbe 100644 --- a/deps/rabbit/priv/schema/rabbit.schema +++ b/deps/rabbit/priv/schema/rabbit.schema @@ -112,6 +112,14 @@ end}. {datatype, {enum, [true, false]}} ]}. +{mapping, "listeners.cli.$proto", "rabbit.cli_listeners",[ + {datatype, [integer, ip]} +]}. +{translation, "rabbit.cli_listeners", +fun(Conf) -> + cuttlefish_variable:filter_by_prefix("listeners.cli", Conf) +end}. + {mapping, "erlang.K", "vm_args.+K", [ {default, "true"}, {level, advanced} diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index 678921a8933c..7ae2b6d6a4e1 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -235,6 +235,13 @@ {requires, [core_initialized, recovery]}, {enables, routing_ready}]}). +-rabbit_boot_step({rabbit_cli_http_listener, + [{description, "RabbitMQ CLI HTTP listener"}, + {mfa, {rabbit_sup, start_restartable_child, + [rabbit_cli_http_listener]}}, + {requires, [core_initialized, recovery]}, + {enables, routing_ready}]}). + -rabbit_boot_step({rabbit_observer_cli, [{description, "Observer CLI configuration"}, {mfa, {rabbit_observer_cli, init, []}}, @@ -951,7 +958,6 @@ start(normal, []) -> %% will be used. We start it now because we can't wait for boot steps %% to do this (feature flags are refreshed before boot steps run). ok = rabbit_sup:start_child(rabbit_ff_controller), - ok = rabbit_sup:start_child(rabbit_cli_ws), %% Compatibility with older RabbitMQ versions + required by %% rabbit_node_monitor:notify_node_up/0: diff --git a/deps/rabbit/src/rabbit_cli2.erl b/deps/rabbit/src/rabbit_cli2.erl new file mode 100644 index 000000000000..b35c45997b1a --- /dev/null +++ b/deps/rabbit/src/rabbit_cli2.erl @@ -0,0 +1,198 @@ +-module(rabbit_cli2). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-export([main/1, + merge_argparse_def/2, + noop/1]). + +-record(?MODULE, {progname, + args, + group_leader, + argparse_def, + arg_map, + cmd_path, + command, + connection}). + +main(Args) -> + Progname = escript:script_name(), + Ret = run_cli(Progname, Args), + io:format(standard_error, "CLI run_cli() -> ~p~n", [Ret]), + erlang:halt(). + +run_cli(Progname, Args) -> + GroupLeader = erlang:group_leader(), + Context = #?MODULE{progname = Progname, + args = Args, + group_leader = GroupLeader}, + add_rabbitmq_code_path(Context). + +add_rabbitmq_code_path(#?MODULE{progname = Progname} = Context) -> + ScriptDir = filename:dirname(Progname), + PluginsDir0 = filename:join([ScriptDir, "..", "plugins"]), + PluginsDir1 = case filelib:is_dir(PluginsDir0) of + true -> + PluginsDir0 + end, + Glob = filename:join([PluginsDir1, "*", "ebin"]), + AppDirs = filelib:wildcard(Glob), + lists:foreach(fun code:add_path/1, AppDirs), + init_local_args(Context). + +init_local_args(Context) -> + maybe + LocalArgparseDef = local_argparse_def(), + Context1 = Context#?MODULE{argparse_def = LocalArgparseDef}, + + {ok, + PartialArgMap, + PartialCmdPath, + PartialCommand} ?= initial_parse(Context1), + Context2 = Context1#?MODULE{arg_map = PartialArgMap, + cmd_path = PartialCmdPath, + command = PartialCommand}, + connect_to_node(Context2) + end. + +connect_to_node(#?MODULE{arg_map = #{node := NodenameOrUri}} = Context) -> + maybe + {ok, Connection} ?= rabbit_cli_transport2:connect(NodenameOrUri), + Context1 = Context#?MODULE{connection = Connection}, + init_final_args(Context1) + end; +connect_to_node(#?MODULE{} = Context) -> + maybe + {ok, Connection} ?= rabbit_cli_transport2:connect(), + Context1 = Context#?MODULE{connection = Connection}, + init_final_args(Context1) + end. + +init_final_args(Context) -> + maybe + %% We can query the argparse definition from the remote node to know + %% the commands it supports and proceed with the execution. + {ok, ArgparseDef} ?= final_argparse_def(Context), + Context1 = Context#?MODULE{argparse_def = ArgparseDef}, + + {ok, + ArgMap, + CmdPath, + Command} ?= final_parse(Context1), + Context2 = Context1#?MODULE{arg_map = ArgMap, + cmd_path = CmdPath, + command = Command}, + + run_command(Context2) + end. + +%% ------------------------------------------------------------------- +%% Arguments definition and parsing. +%% ------------------------------------------------------------------- + +local_argparse_def() -> + #{arguments => + [ + #{name => help, + long => "-help", + short => $h, + type => boolean, + help => "Display help and exit"}, + #{name => node, + long => "-node", + short => $n, + type => string, + nargs => 1, + help => "Name of the node to control"}, + #{name => verbose, + long => "-verbose", + short => $v, + action => count, + help => + "Be verbose; can be specified multiple times to increase verbosity"}, + #{name => version, + long => "-version", + short => $V, + type => boolean, + help => + "Display version and exit"} + ], + + handler => {?MODULE, noop}}. + +initial_parse( + #?MODULE{progname = Progname, args = Args, argparse_def = ArgparseDef}) -> + Options = #{progname => Progname}, + case partial_parse(Args, ArgparseDef, Options) of + {ok, ArgMap, CmdPath, Command, _RemainingArgs} -> + {ok, ArgMap, CmdPath, Command}; + {error, _} = Error-> + Error + end. + +partial_parse(Args, ArgparseDef, Options) -> + partial_parse(Args, ArgparseDef, Options, []). + +partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> + case argparse:parse(Args, ArgparseDef, Options) of + {ok, ArgMap, CmdPath, Command} -> + RemainingArgs1 = lists:reverse(RemainingArgs), + {ok, ArgMap, CmdPath, Command, RemainingArgs1}; + {error, {_CmdPath, undefined, Arg, <<>>}} -> + Args1 = Args -- [Arg], + RemainingArgs1 = [Arg | RemainingArgs], + partial_parse(Args1, ArgparseDef, Options, RemainingArgs1); + {error, _} = Error -> + Error + end. + +final_argparse_def(#?MODULE{argparse_def = PartialArgparseDef} = Context) -> + maybe + {ok, FullArgparseDef} ?= get_full_argparse_def(Context), + ArgparseDef1 = merge_argparse_def(PartialArgparseDef, FullArgparseDef), + {ok, ArgparseDef1} + end. + +get_full_argparse_def(#?MODULE{connection = Connection}) -> + RemoteArgparseDef = rabbit_cli_transport2:rpc( + Connection, rabbit_cli_commands, argparse_def, []), + {ok, RemoteArgparseDef}; +get_full_argparse_def(_) -> + LocalArgparseDef = rabbit_cli_commands:argparse_def(), + {ok, LocalArgparseDef}. + +merge_argparse_def(ArgparseDef1, ArgparseDef2) -> + Args1 = maps:get(arguments, ArgparseDef1, []), + Args2 = maps:get(arguments, ArgparseDef2, []), + Args = merge_arguments(Args1, Args2), + Cmds1 = maps:get(commands, ArgparseDef1, #{}), + Cmds2 = maps:get(commands, ArgparseDef2, #{}), + Cmds = merge_commands(Cmds1, Cmds2), + maps:merge( + ArgparseDef1, + ArgparseDef2#{arguments => Args, commands => Cmds}). + +merge_arguments(Args1, Args2) -> + Args1 ++ Args2. + +merge_commands(Cmds1, Cmds2) -> + maps:merge(Cmds1, Cmds2). + +final_parse( + #?MODULE{progname = Progname, args = Args, argparse_def = ArgparseDef}) -> + Options = #{progname => Progname}, + argparse:parse(Args, ArgparseDef, Options). + +%% ------------------------------------------------------------------- +%% Command execution. +%% ------------------------------------------------------------------- + +run_command(#?MODULE{connection = Connection} = Context) -> + rabbit_cli_transport2:rpc( + Connection, rabbit_cli_commands, run_command, [Context]); +run_command(Context) -> + rabbit_cli_commands:run_command(Context). + +noop(_Context) -> + ok. diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl new file mode 100644 index 000000000000..c37caac0d921 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -0,0 +1,186 @@ +-module(rabbit_cli_http_client). + +-behaviour(gen_statem). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/1, t/0, + rpc/4, + send/3]). +-export([init/1, + callback_mode/0, + handle_event/4, + terminate/3, + code_change/4]). + +-record(?MODULE, {uri :: uri_string:uri_map(), + connection :: pid(), + stream :: gun:stream_ref(), + delayed_requests = [] :: list(), + io_requests = #{} :: map(), + group_leader :: pid()}). + +t() -> + {ok, P} = start_link("http://localhost:8080"), + Data = rpc(P, io, get_line, ["Prompt: "]), + rpc(P, io, format, ["Data: ~p~n", [Data]]). + +start_link(Uri) -> + gen_statem:start_link(?MODULE, Uri, []). + +rpc(Client, Module, Function, Args) -> + gen_statem:call(Client, {?FUNCTION_NAME, Module, Function, Args}). + +send(Client, Dest, Msg) -> + gen_statem:cast(Client, {?FUNCTION_NAME, Dest, Msg}). + +init(Uri) -> + maybe + #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), + GroupLeader = erlang:group_leader(), + + {ok, _} ?= application:ensure_all_started(gun), + + ?LOG_DEBUG("CLI: opening HTTP connection to ~s:~b", [Host, Port]), + {ok, ConnPid} ?= gun:open(Host, Port), + + Data = #?MODULE{uri = UriMap, + group_leader = GroupLeader, + connection = ConnPid}, + {ok, opening_connection, Data} + end. + +callback_mode() -> + handle_event_function. + +handle_event( + info, {gun_up, ConnPid, _}, + opening_connection, + #?MODULE{connection = ConnPid} = Data) -> + ?LOG_DEBUG("CLI: HTTP connection opened, upgrading to websocket"), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + Data1 = Data#?MODULE{stream = StreamRef}, + {next_state, opening_stream, Data1}; +handle_event( + info, {gun_upgrade, _ConnPid, _StreamRef, _Frames, _}, + opening_stream, + #?MODULE{} = Data) -> + ?LOG_DEBUG("CLI: websocket ready, sending pending requests"), + Data1 = flush_delayed_requests(Data), + {next_state, stream_ready, Data1}; +%% Call (e.g. RPC). +handle_event( + {call, From}, Command, + stream_ready, + #?MODULE{} = Data) -> + Request = prepare_call(From, Command), + send_request(Request, Data), + {keep_state, Data}; +handle_event( + {call, From}, Command, + _State, + #?MODULE{} = Data) -> + Request = prepare_call(From, Command), + Data1 = delay_request(Request, Data), + {keep_state, Data1}; +%% Cast (e.g. send). +handle_event( + cast, Command, + stream_ready, + #?MODULE{} = Data) -> + Request = prepare_cast(Command), + send_request(Request, Data), + {keep_state, Data}; +handle_event( + cast, Command, + _State, + #?MODULE{} = Data) -> + Request = prepare_cast(Command), + Data1 = delay_request(Request, Data), + {keep_state, Data1}; +handle_event( + info, {gun_ws, _ConnPid, _StreamRef, {binary, RequestBin}}, + stream_ready, + #?MODULE{} = Data) -> + Request = binary_to_term(RequestBin), + ?LOG_DEBUG("CLI: received request from server: ~p", [Request]), + case handle_request(Request, Data) of + {reply, Reply, Data1} -> + send_request(Reply, Data1), + {keep_state, Data1}; + {noreply, Data1} -> + {keep_state, Data1} + end; +handle_event( + info, {io_reply, ProxyRef, Reply}, + _State, + #?MODULE{io_requests = IoRequests} = Data) -> + {From, ReplyAs} = maps:get(ProxyRef, IoRequests), + IoReply = {io_reply, ReplyAs, Reply}, + Command = {send, From, IoReply}, + Request = prepare_cast(Command), + send_request(Request, Data), + IoRequests1 = maps:remove(ProxyRef, IoRequests), + Data1 = Data#?MODULE{io_requests = IoRequests1}, + {keep_state, Data1}; +handle_event( + info, {gun_ws, _ConnPid, _StreamRef, {close, _, _}}, + stream_ready, + #?MODULE{} = Data) -> + ?LOG_DEBUG("CLI: stream closed"), + %% FIXME: Handle pending requests. + {stop, normal, Data}; +handle_event( + info, {gun_down, _ConnPid, _Proto, _Reason, _KilledStreams}, + _State, + #?MODULE{} = Data) -> + ?LOG_DEBUG("CLI: gun_down: ~p", [_Reason]), + %% FIXME: Handle pending requests. + {stop, normal, Data}. + +terminate(_Reason, _State, _Data) -> + ok. + +code_change(_Vsn, State, Data, _Extra) -> + {ok, State, Data}. + +prepare_call(From, Command) -> + {call, From, Command}. + +prepare_cast(Command) -> + {cast, Command}. + +send_request( + Request, + #?MODULE{connection = ConnPid, stream = StreamRef}) -> + RequestBin = term_to_binary(Request), + Frame = {binary, RequestBin}, + gun:ws_send(ConnPid, StreamRef, Frame). + +delay_request(Request, #?MODULE{delayed_requests = Requests} = Data) -> + Requests1 = [Request | Requests], + Data1 = Data#?MODULE{delayed_requests = Requests1}, + Data1. + +flush_delayed_requests(#?MODULE{delayed_requests = Requests} = Data) -> + lists:foreach( + fun(Request) -> send_request(Request, Data) end, + lists:reverse(Requests)), + Data1 = Data#?MODULE{delayed_requests = []}, + Data1. + +handle_request({call_ret, From, Reply}, Data) -> + gen_statem:reply(From, Reply), + {noreply, Data}; +handle_request({call_exception, Class, Reason, Stacktrace}, _Data) -> + erlang:raise(Class, Reason, Stacktrace); +handle_request( + {io_request, From, ReplyAs, Request}, + #?MODULE{group_leader = GroupLeader, + io_requests = IoRequests} = Data) -> + ProxyRef = erlang:make_ref(), + ProxyIoRequest = {io_request, self(), ProxyRef, Request}, + GroupLeader ! ProxyIoRequest, + IoRequests1 = IoRequests#{ProxyRef => {From, ReplyAs}}, + Data1 = Data#?MODULE{io_requests = IoRequests1}, + {noreply, Data1}. diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl new file mode 100644 index 000000000000..4d91ec515674 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_http_listener.erl @@ -0,0 +1,199 @@ +-module(rabbit_cli_http_listener). + +-behaviour(gen_server). +-behaviour(cowboy_websocket). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/0]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + config_change/3]). +-export([init/2, + websocket_init/1, + websocket_handle/2, + websocket_info/2, + terminate/3]). + +-record(?MODULE, {listeners = [] :: [pid()]}). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, #{}, []). + +%% ------------------------------------------------------------------- +%% Top-level gen_server. +%% ------------------------------------------------------------------- + +init(_) -> + process_flag(trap_exit, true), + case start_listeners() of + {ok, []} -> + ignore; + {ok, Listeners} -> + State = #?MODULE{listeners = Listeners}, + {ok, State, hibernate}; + {error, _} = Error -> + Error + end. + +handle_call(Request, From, State) -> + ?LOG_DEBUG("CLI: unhandled call from ~0p: ~p", [From, Request]), + {reply, ok, State}. + +handle_cast(Request, State) -> + ?LOG_DEBUG("CLI: unhandled cast: ~p", [Request]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #?MODULE{listeners = Listeners}) -> + stop_listeners(Listeners). + +%% ------------------------------------------------------------------- +%% HTTP listeners management. +%% ------------------------------------------------------------------- + +start_listeners() -> + case application:get_env(rabbit, cli_listeners) of + undefined -> + ?LOG_INFO("CLI: no HTTP(S) listeners started"), + {ok, []}; + {ok, Listeners} when is_list(Listeners) -> + start_listeners(Listeners, []) + end. + +start_listeners( + [{[_, _, "http" = Proto], Port} | Rest], Result) when is_integer(Port) -> + ?LOG_INFO("CLI: starting \"~s\" listener on TCP port ~b", [Proto, Port]), + Name = list_to_binary(io_lib:format("cli_listener_~s_~b", [Proto, Port])), + case start_listener(Name, Port) of + {ok, Pid} -> + Result1 = [{Proto, Port, Pid} | Result], + start_listeners(Rest, Result1); + {error, Reason} -> + ?LOG_ERROR( + "CLI: failed to start \"~s\" listener on TCP port ~b: ~0p", + [Proto, Port, Reason]), + start_listeners(Rest, Result) + end; +start_listeners([], Result) -> + Result1 = lists:reverse(Result), + {ok, Result1}. + +start_listener(Name, Port) -> + Dispatch = cowboy_router:compile([{'_', [{'_', ?MODULE, #{}}]}]), + cowboy:start_clear(Name, + [{port, Port}], + #{env => #{dispatch => Dispatch}} + ). + +stop_listeners([{Proto, Port, Pid} | Rest]) -> + ?LOG_INFO("CLI: stopping \"~s\" listener on TCP port ~b", [Proto, Port]), + _ = cowboy:stop_listener(Pid), + stop_listeners(Rest); +stop_listeners([]) -> + ok. + +config_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% ------------------------------------------------------------------- +%% Cowboy handler. +%% ------------------------------------------------------------------- + +init(#{method := <<"GET">>} = Req, State) -> + ?LOG_DEBUG("CLI: received HTTP request: ~p", [Req]), + UpgradeHeader = cowboy_req:header(<<"upgrade">>, Req), + case UpgradeHeader of + <<"websocket">> -> + {cowboy_websocket, Req, State, #{idle_timeout => 30000}}; + _ -> + case Req of + #{path := Path} + when Path =:= <<"">> orelse Path =:= <<"/index.html">> -> + Req1 = reply_with_help(Req, 200), + {ok, Req1, State}; + _ -> + Req1 = reply_with_help(Req, 404), + {ok, Req1, State} + end + end; +init(Req, State) -> + ?LOG_DEBUG("CLI: received HTTP request: ~p", [Req]), + Req1 = reply_with_help(Req, 405), + {ok, Req1, State}. + +websocket_init(State) -> + {ok, Server} = rabbit_cli_http_server:start_link(self()), + State1 = State#{server => Server, + reqids => gen_server:reqids_new()}, + {ok, State1}. + +websocket_handle( + {binary, RequestBin}, + #{server := Server, reqids := ReqIds} = State) -> + Request = binary_to_term(RequestBin), + ?LOG_DEBUG("CLI: received request from client: ~p", [Request]), + ReqIds1 = rabbit_cli_http_server:send_request( + Server, Request, undefined, ReqIds), + State1 = State#{reqids => ReqIds1}, + {ok, State1}; +websocket_handle(Frame, State) -> + ?LOG_DEBUG("CLI: unhandled Websocket frame: ~p", [Frame]), + {ok, State}. + +websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> + IoRequestBin = term_to_binary(IoRequest), + Frame = {binary, IoRequestBin}, + {[Frame], State}; +websocket_info(Info, #{server := Server, reqids := ReqIds} = State) -> + case gen_server:check_response(Info, ReqIds, true) of + {{reply, Response}, _Label, ReqIds1} -> + State1 = State#{reqids => ReqIds1}, + case Response of + {reply, Reply} -> + ReplyBin = term_to_binary(Reply), + Frame = {binary, ReplyBin}, + {[Frame], State1}; + noreply -> + {ok, State1} + end; + {{error, {Reason, Server}}, _Label, ReqIds1} -> + State1 = State#{reqids => ReqIds1}, + ?LOG_DEBUG("CLI: error from gen_server request: ~p", [Reason]), + {ok, State1}; + NotResponse + when NotResponse =:= no_request orelse NotResponse =:= no_reply -> + ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), + {ok, State} + end. + +terminate(_Reason, _Req, #{server := Server}) -> + ?LOG_ALERT("CLI: terminate: ~p", [_Reason]), + rabbit_cli_http_server:stop(Server), + receive + {'EXIT', Server, _} -> + ok + end, + ok; +terminate(_Reason, _Req, _State) -> + ?LOG_ALERT("CLI: terminate: ~p", [_Reason]), + ok. + +reply_with_help(Req, Code) -> + PrivDir = code:priv_dir(rabbit), + HelpFilename = filename:join(PrivDir, "cli_http_help.html"), + Body = case file:read_file(HelpFilename) of + {ok, Content} -> + Content; + {error, _} -> + <<>> + end, + cowboy_req:reply( + Code, #{<<"content-type">> => <<"text/html; charset=utf-8">>}, Body, + Req). diff --git a/deps/rabbit/src/rabbit_cli_http_server.erl b/deps/rabbit/src/rabbit_cli_http_server.erl new file mode 100644 index 000000000000..c87b7b685865 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_http_server.erl @@ -0,0 +1,81 @@ +-module(rabbit_cli_http_server). + +-behaviour(gen_server). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/1, + send_request/4, + stop/1]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + config_change/3]). + +start_link(Websocket) -> + gen_server:start_link(?MODULE, #{websocket => Websocket}, []). + +send_request(_Server, {cast, {send, _Dest, _Msg} = Command}, _Labet, ReqIds) -> + %% Bypass server to send messages. This is because the server might be + %% busy waiting for that message, in which case it can't receive a command + %% to send it to itself. + _ = handle_command(Command), + ReqIds; +send_request(Server, Request, Label, ReqIds) -> + gen_server:send_request(Server, Request, Label, ReqIds). + +stop(Server) -> + gen_server:stop(Server). + +%% ------------------------------------------------------------------- +%% gen_server hanling a single websocket connection. +%% ------------------------------------------------------------------- + +init(#{websocket := Websocket} = Args) -> + process_flag(trap_exit, true), + erlang:group_leader(Websocket, self()), + {ok, Args}. + +handle_call({call, From, Command}, _From, State) -> + try + Ret = handle_command(Command), + Reply = {call_ret, From, Ret}, + {reply, {reply, Reply}, State} + catch + Class:Reason:Stacktrace -> + Exception = {call_exception, Class, Reason, Stacktrace}, + {reply, {reply, Exception}, State} + end; +handle_call({cast, Command}, _From, State) -> + try + _ = handle_command(Command), + {reply, noreply, State} + catch + Class:Reason:Stacktrace -> + Exception = {call_exception, Class, Reason, Stacktrace}, + {reply, {reply, Exception}, State} + end; +handle_call(Request, From, State) -> + ?LOG_DEBUG("CLI: unhandled call from ~0p: ~p", [From, Request]), + {reply, ok, State}. + +handle_cast(Request, State) -> + ?LOG_DEBUG("CLI: unhandled cast: ~p", [Request]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +config_change(_OldVsn, State, _Extra) -> + {ok, State}. + +handle_command({rpc, Module, Function, Args}) -> + erlang:apply(Module, Function, Args); +handle_command({send, Dest, Msg}) -> + erlang:send(Dest, Msg). diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl new file mode 100644 index 000000000000..18d733120ce3 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -0,0 +1,86 @@ +-module(rabbit_cli_transport2). + +-export([connect/0, connect/1, + rpc/4, + send/3]). + +-record(?MODULE, {type :: erldist | http, + peer :: atom() | pid()}). + +connect() -> + Nodename = guess_rabbitmq_nodename(), + connect(erldist, Nodename). + +connect(NodenameOrUri) -> + Proto = determine_proto(NodenameOrUri), + connect(Proto, NodenameOrUri). + +connect(erldist = Proto, Nodename) -> + maybe + Nodename1 = complete_nodename(Nodename), + {ok, _} ?= net_kernel:start(undefined, #{name_domain => shortnames}), + + %% Can we reach the remote node? + case net_kernel:connect_node(Nodename1) of + true -> + Connection = #?MODULE{type = Proto, + peer = Nodename1}, + {ok, Connection}; + false -> + {error, noconnection} + end + end; +connect(http = Proto, Uri) -> + maybe + {ok, Client} = rabbit_cli_http_client:start_link(Uri), + Connection = #?MODULE{type = Proto, + peer = Client}, + {ok, Connection} + end. + +guess_rabbitmq_nodename() -> + case net_adm:names() of + {ok, NamesAndPorts} -> + Names0 = [Name || {Name, _Port} <- NamesAndPorts], + Names1 = lists:sort(Names0), + Names2 = lists:filter( + fun + ("rabbit" ++ _) -> true; + (_) -> false + end, Names1), + case Names2 of + [First | _] -> + First; + [] -> + "rabbit" + end; + {error, address} -> + "rabbit" + end. + +determine_proto(NodenameOrUri) -> + case re:run(NodenameOrUri, "://", [{capture, none}]) of + nomatch -> + erldist; + match -> + http + end. + +complete_nodename(Nodename) -> + case re:run(Nodename, "@", [{capture, none}]) of + nomatch -> + {ok, ThisHost} = inet:gethostname(), + list_to_atom(Nodename ++ "@" ++ ThisHost); + match -> + list_to_atom(Nodename) + end. + +rpc(#?MODULE{type = erldist}, Module, Function, Args) -> + erlang:apply(Module, Function, Args); +rpc(#?MODULE{type = http, peer = Pid}, Module, Function, Args) -> + rabbit_cli_http_client:rpc(Pid, Module, Function, Args). + +send(#?MODULE{type = erldist}, Dest, Msg) -> + erlang:send(Dest, Msg); +send(#?MODULE{type = http, peer = Pid}, Dest, Msg) -> + rabbit_cli_http_client:send(Pid, Dest, Msg). From 4cd8ee8734278d95bfcf7b05556e9771eb5831c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 13 Jun 2025 10:44:47 +0200 Subject: [PATCH 14/51] WIP --- deps/rabbit/Makefile | 4 +- deps/rabbit/src/rabbit_cli_backend.erl | 105 ++++++++++++++++++ deps/rabbit/src/rabbit_cli_backend.hrl | 6 + ...abbit_cli2.erl => rabbit_cli_frontend.erl} | 32 +++++- 4 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 deps/rabbit/src/rabbit_cli_backend.erl create mode 100644 deps/rabbit/src/rabbit_cli_backend.hrl rename deps/rabbit/src/{rabbit_cli2.erl => rabbit_cli_frontend.erl} (84%) diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index 253d7b7e27fa..98ac362c4152 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -158,12 +158,12 @@ DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk include ../../rabbitmq-components.mk include ../../erlang.mk -ESCRIPT_NAME := rabbit_cli2 +ESCRIPT_NAME := rabbit_cli_frontend ESCRIPT_FILE := scripts/rmq ebin/$(PROJECT).app:: $(ESCRIPT_FILE) -$(ESCRIPT_FILE): ebin/rabbit_cli2.beam +$(ESCRIPT_FILE): ebin/rabbit_cli_frontend.beam $(gen_verbose) printf "%s\n" \ "#!$(ESCRIPT_SHEBANG)" \ "%% $(ESCRIPT_COMMENT)" \ diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl new file mode 100644 index 000000000000..826c435bfb16 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -0,0 +1,105 @@ +-module(rabbit_cli_backend). + +-include_lib("kernel/include/logger.hrl"). + +-include_lib("rabbit_common/include/logging.hrl"). +-include_lib("rabbit_common/include/resource.hrl"). + +-include("src/rabbit_cli_backend.hrl"). + +-export([final_argparse_def/0, run_command/1]). + +%% ------------------------------------------------------------------- +%% Commands discovery. +%% ------------------------------------------------------------------- + +final_argparse_def() -> + #{argparse_def := ArgparseDef} = get_discovered_commands(), + ArgparseDef. + +get_discovered_commands() -> + Key = {?MODULE, discovered_commands}, + try + persistent_term:get(Key) + catch + error:badarg -> + Commands = discover_commands(), + ArgparseDef = commands_to_cli_argparse_def(Commands), + Cache = #{commands => Commands, + argparse_def => ArgparseDef}, + persistent_term:put(Key, Cache), + Cache + end. + +discover_commands() -> + %% Extract the commands from module attributes like feature flags and boot + %% steps. + ?LOG_DEBUG( + "Commands: query commands in loaded applications", + #{domain => ?RMQLOG_DOMAIN_CMD}), + T0 = erlang:monotonic_time(), + ScannedApps = rabbit_misc:rabbitmq_related_apps(), + AttrsPerApp = rabbit_misc:module_attributes_from_apps( + rabbitmq_command, ScannedApps), + T1 = erlang:monotonic_time(), + ?LOG_DEBUG( + "Commands: time to find supported commands: ~tp us", + [erlang:convert_time_unit(T1 - T0, native, microsecond)], + #{domain => ?RMQLOG_DOMAIN_CMD}), + AttrsPerApp. + +commands_to_cli_argparse_def(Commands) -> + lists:foldl( + fun({_App, _Mod, Entries}, Acc0) -> + lists:foldl( + fun + ({#{cli := Path}, Def}, Acc1) -> + Def1 = expand_argparse_def(Def), + M1 = lists:foldr( + fun + (Cmd, undefined) -> + #{commands => #{Cmd => Def1}}; + (Cmd, M0) -> + #{commands => #{Cmd => M0}} + end, undefined, Path), + rabbit_cli:merge_argparse_def(Acc1, M1); + (_, Acc1) -> + Acc1 + end, Acc0, Entries) + end, #{}, Commands). + +expand_argparse_def(Def) when is_map(Def) -> + Def; +expand_argparse_def(Defs) when is_list(Defs) -> + lists:foldl( + fun + (argparse_def_record_stream, Acc) -> + Def = rabbit_cli_io:argparse_def(record_stream), + rabbit_cli:merge_argparse_def(Acc, Def); + (argparse_def_file_input, Acc) -> + Def = rabbit_cli_io:argparse_def(file_input), + rabbit_cli:merge_argparse_def(Acc, Def); + (Def, Acc) -> + Def1 = expand_argparse_def(Def), + rabbit_cli:merge_argparse_def(Acc, Def1) + end, #{}, Defs). + +%% ------------------------------------------------------------------- +%% Commands execution. +%% ------------------------------------------------------------------- + +run_command(ContextMap) -> + Context = map_to_context(ContextMap), + do_run_command(Context). + +do_run_command( + #rabbit_cli{command = #{handler := {Module, Function}}} = Context) -> + erlang:apply(Module, Function, [Context]). + +map_to_context(ContextMap) -> + #rabbit_cli{progname = maps:get(progname, ContextMap), + args = maps:get(args, ContextMap), + argparse_def = maps:get(argparse_def, ContextMap), + arg_map = maps:get(arg_map, ContextMap), + cmd_path = maps:get(cmd_path, ContextMap), + command = maps:get(command, ContextMap)}. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl new file mode 100644 index 000000000000..ffccf6873365 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -0,0 +1,6 @@ +-record(rabbit_cli, {progname, + args, + argparse_def, + arg_map, + cmd_path, + command}). diff --git a/deps/rabbit/src/rabbit_cli2.erl b/deps/rabbit/src/rabbit_cli_frontend.erl similarity index 84% rename from deps/rabbit/src/rabbit_cli2.erl rename to deps/rabbit/src/rabbit_cli_frontend.erl index b35c45997b1a..9ce6a32efce7 100644 --- a/deps/rabbit/src/rabbit_cli2.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -1,4 +1,4 @@ --module(rabbit_cli2). +-module(rabbit_cli_frontend). -include_lib("kernel/include/logger.hrl"). -include_lib("stdlib/include/assert.hrl"). @@ -8,8 +8,8 @@ noop/1]). -record(?MODULE, {progname, - args, group_leader, + args, argparse_def, arg_map, cmd_path, @@ -155,11 +155,13 @@ final_argparse_def(#?MODULE{argparse_def = PartialArgparseDef} = Context) -> end. get_full_argparse_def(#?MODULE{connection = Connection}) -> + %% TODO: Handle an undef failure when the remote node is too old. RemoteArgparseDef = rabbit_cli_transport2:rpc( - Connection, rabbit_cli_commands, argparse_def, []), + Connection, + rabbit_cli_backend, final_argparse_def, []), {ok, RemoteArgparseDef}; get_full_argparse_def(_) -> - LocalArgparseDef = rabbit_cli_commands:argparse_def(), + LocalArgparseDef = rabbit_cli_backend:final_argparse_def(), {ok, LocalArgparseDef}. merge_argparse_def(ArgparseDef1, ArgparseDef2) -> @@ -189,10 +191,28 @@ final_parse( %% ------------------------------------------------------------------- run_command(#?MODULE{connection = Connection} = Context) -> + ContextMap = context_to_map(Context), rabbit_cli_transport2:rpc( - Connection, rabbit_cli_commands, run_command, [Context]); + Connection, rabbit_cli_backend, run_command, [ContextMap]); run_command(Context) -> - rabbit_cli_commands:run_command(Context). + %% FIXME: Do we need to spawn a process? + ContextMap = context_to_map(Context), + rabbit_cli_backend:run_command(ContextMap). + +context_to_map(Context) -> + Fields = [Field || Field <- record_info(fields, ?MODULE), + %% We don’t need or want to communicate the connection + %% state or the group leader to the backend. + Field =/= connection orelse + Field =/= group_leader], + record_to_map(Fields, Context, 2, #{}). + +record_to_map([Field | Rest], Record, Index, Map) -> + Value = element(Index, Record), + Map1 = Map#{Field => Value}, + record_to_map(Rest, Record, Index + 1, Map1); +record_to_map([], _Record, _Index, Map) -> + Map. noop(_Context) -> ok. From 6168d3a894e90102d4e6a49307041d945add05c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 12:17:27 +0200 Subject: [PATCH 15/51] WIP --- deps/rabbit/src/rabbit_cli_frontend.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 9ce6a32efce7..fce2a8c3e001 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -58,6 +58,14 @@ init_local_args(Context) -> connect_to_node(#?MODULE{arg_map = #{node := NodenameOrUri}} = Context) -> maybe + %% TODO: Send a list of supported features: + %% * support for some messages, like Erlang I/O protocol, file + %% read/write support + %% * type of terminal (or no terminal) + %% * capabilities of the terminal + %% * is plain test or HTTP + %% * evolutions in the communication between the frontend and the + %% backend {ok, Connection} ?= rabbit_cli_transport2:connect(NodenameOrUri), Context1 = Context#?MODULE{connection = Connection}, init_final_args(Context1) From c4e2f56cdc2282d32f16810874be589a4d16e72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 16:30:23 +0200 Subject: [PATCH 16/51] Name the CLI "rabbitmq" --- deps/rabbit/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index 98ac362c4152..e347cbeab068 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -159,7 +159,7 @@ include ../../rabbitmq-components.mk include ../../erlang.mk ESCRIPT_NAME := rabbit_cli_frontend -ESCRIPT_FILE := scripts/rmq +ESCRIPT_FILE := scripts/rabbitmq ebin/$(PROJECT).app:: $(ESCRIPT_FILE) From e9db4a5115c0ba46f79fe2db51fba0d8bd4db359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 17:13:22 +0200 Subject: [PATCH 17/51] Use rabbit_misc:rabbitmq_related_module_attributes/1 directly --- deps/rabbit/src/rabbit_cli_backend.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 826c435bfb16..fdb3f08b838f 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -38,9 +38,8 @@ discover_commands() -> "Commands: query commands in loaded applications", #{domain => ?RMQLOG_DOMAIN_CMD}), T0 = erlang:monotonic_time(), - ScannedApps = rabbit_misc:rabbitmq_related_apps(), - AttrsPerApp = rabbit_misc:module_attributes_from_apps( - rabbitmq_command, ScannedApps), + AttrsPerApp = rabbit_misc:rabbitmq_related_module_attributes( + rabbitmq_command), T1 = erlang:monotonic_time(), ?LOG_DEBUG( "Commands: time to find supported commands: ~tp us", From 14833bcef4cb5d7379f8ef0df10644c37fae5f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 17:14:13 +0200 Subject: [PATCH 18/51] Fix erldist rpc --- deps/rabbit/src/rabbit_cli_transport2.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index 18d733120ce3..f1f6e635e48a 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -75,8 +75,8 @@ complete_nodename(Nodename) -> list_to_atom(Nodename) end. -rpc(#?MODULE{type = erldist}, Module, Function, Args) -> - erlang:apply(Module, Function, Args); +rpc(#?MODULE{type = erldist, peer = Node}, Module, Function, Args) -> + erpc:call(Node, Module, Function, Args); rpc(#?MODULE{type = http, peer = Pid}, Module, Function, Args) -> rabbit_cli_http_client:rpc(Pid, Module, Function, Args). From e6c6a624696152ec60c6898773938e63de1da1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 17:15:40 +0200 Subject: [PATCH 19/51] Put scriptname and progname separately in context --- deps/rabbit/src/rabbit_cli_backend.erl | 3 ++- deps/rabbit/src/rabbit_cli_backend.hrl | 3 ++- deps/rabbit/src/rabbit_cli_frontend.erl | 27 ++++++++++++++----------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index fdb3f08b838f..50bff935e32f 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -96,7 +96,8 @@ do_run_command( erlang:apply(Module, Function, [Context]). map_to_context(ContextMap) -> - #rabbit_cli{progname = maps:get(progname, ContextMap), + #rabbit_cli{scriptname = maps:get(scriptname, ContextMap), + progname = maps:get(progname, ContextMap), args = maps:get(args, ContextMap), argparse_def = maps:get(argparse_def, ContextMap), arg_map = maps:get(arg_map, ContextMap), diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index ffccf6873365..9a4b6e762648 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -1,4 +1,5 @@ --record(rabbit_cli, {progname, +-record(rabbit_cli, {scriptname, + progname, args, argparse_def, arg_map, diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index fce2a8c3e001..bf16b1c7cd97 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -7,7 +7,8 @@ merge_argparse_def/2, noop/1]). --record(?MODULE, {progname, +-record(?MODULE, {scriptname, + progname, group_leader, args, argparse_def, @@ -17,20 +18,22 @@ connection}). main(Args) -> - Progname = escript:script_name(), - Ret = run_cli(Progname, Args), + ScriptName = escript:script_name(), + Ret = run_cli(ScriptName, Args), io:format(standard_error, "CLI run_cli() -> ~p~n", [Ret]), erlang:halt(). -run_cli(Progname, Args) -> +run_cli(ScriptName, Args) -> + ProgName = filename:basename(ScriptName, ".escript"), GroupLeader = erlang:group_leader(), - Context = #?MODULE{progname = Progname, + Context = #?MODULE{scriptname = ScriptName, + progname = ProgName, args = Args, group_leader = GroupLeader}, add_rabbitmq_code_path(Context). -add_rabbitmq_code_path(#?MODULE{progname = Progname} = Context) -> - ScriptDir = filename:dirname(Progname), +add_rabbitmq_code_path(#?MODULE{scriptname = ScriptName} = Context) -> + ScriptDir = filename:dirname(ScriptName), PluginsDir0 = filename:join([ScriptDir, "..", "plugins"]), PluginsDir1 = case filelib:is_dir(PluginsDir0) of true -> @@ -130,12 +133,12 @@ local_argparse_def() -> handler => {?MODULE, noop}}. initial_parse( - #?MODULE{progname = Progname, args = Args, argparse_def = ArgparseDef}) -> - Options = #{progname => Progname}, + #?MODULE{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> + Options = #{progname => ProgName}, case partial_parse(Args, ArgparseDef, Options) of {ok, ArgMap, CmdPath, Command, _RemainingArgs} -> {ok, ArgMap, CmdPath, Command}; - {error, _} = Error-> + {error, _} = Error -> Error end. @@ -190,8 +193,8 @@ merge_commands(Cmds1, Cmds2) -> maps:merge(Cmds1, Cmds2). final_parse( - #?MODULE{progname = Progname, args = Args, argparse_def = ArgparseDef}) -> - Options = #{progname => Progname}, + #?MODULE{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> + Options = #{progname => ProgName}, argparse:parse(Args, ArgparseDef, Options). %% ------------------------------------------------------------------- From fdc5a47a1e23b603f8b714a21bda7ccb121c2b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 17:16:03 +0200 Subject: [PATCH 20/51] Add FIXME comment --- deps/rabbit/src/rabbit_cli_frontend.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index bf16b1c7cd97..e584dbcb7722 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -165,13 +165,16 @@ final_argparse_def(#?MODULE{argparse_def = PartialArgparseDef} = Context) -> {ok, ArgparseDef1} end. -get_full_argparse_def(#?MODULE{connection = Connection}) -> +get_full_argparse_def(#?MODULE{connection = Connection}) + when Connection =/= undefined -> %% TODO: Handle an undef failure when the remote node is too old. RemoteArgparseDef = rabbit_cli_transport2:rpc( Connection, rabbit_cli_backend, final_argparse_def, []), {ok, RemoteArgparseDef}; -get_full_argparse_def(_) -> +get_full_argparse_def(#?MODULE{}) -> + %% FIXME: Load applications first, otherwise module attributes are + %% unavailable. LocalArgparseDef = rabbit_cli_backend:final_argparse_def(), {ok, LocalArgparseDef}. From f3611c9d81333cc1525a51573c2df5f8db608d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 17 Jun 2025 17:52:04 +0200 Subject: [PATCH 21/51] Create rabbitmq.escript for easy exec on Windows --- deps/rabbit/Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index e347cbeab068..db5d39a9cec9 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -163,13 +163,17 @@ ESCRIPT_FILE := scripts/rabbitmq ebin/$(PROJECT).app:: $(ESCRIPT_FILE) -$(ESCRIPT_FILE): ebin/rabbit_cli_frontend.beam +$(ESCRIPT_FILE): $(ESCRIPT_FILE).escript + $(gen_verbose) rm -f "$@" + $(verbose) ln -s "$(notdir $(ESCRIPT_FILE)).escript" "$(ESCRIPT_FILE)" + +$(ESCRIPT_FILE).escript: ebin/rabbit_cli_frontend.beam $(gen_verbose) printf "%s\n" \ "#!$(ESCRIPT_SHEBANG)" \ "%% $(ESCRIPT_COMMENT)" \ - "%%! $(ESCRIPT_EMU_ARGS)" > $(ESCRIPT_FILE) - $(verbose) cat $< >> $(ESCRIPT_FILE) - $(verbose) chmod a+x $(ESCRIPT_FILE) + "%%! $(ESCRIPT_EMU_ARGS)" > "$@" + $(verbose) cat $< >> "$@" + $(verbose) chmod a+x "$@" clean:: clean-cli From 4a6319c892a9344d0c42d60605a752b39ef77806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 18 Jun 2025 11:41:37 +0200 Subject: [PATCH 22/51] Discover commands in a boot step --- deps/rabbit/src/rabbit.erl | 7 ++ deps/rabbit/src/rabbit_cli_backend.erl | 126 ++++++++++-------------- deps/rabbit/src/rabbit_cli_commands.erl | 65 ++++++------ 3 files changed, 87 insertions(+), 111 deletions(-) diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index 7ae2b6d6a4e1..a5780fc4e81d 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -235,6 +235,13 @@ {requires, [core_initialized, recovery]}, {enables, routing_ready}]}). +%% CLI-related boot steps. +-rabbit_boot_step({rabbit_cli_command_discovery, + [{description, "RabbitMQ CLI command discovery"}, + {mfa, {rabbit_cli_commands, discover_commands, + []}}, + {requires, [core_initialized, recovery]}, + {enables, routing_ready}]}). -rabbit_boot_step({rabbit_cli_http_listener, [{description, "RabbitMQ CLI HTTP listener"}, {mfa, {rabbit_sup, start_restartable_child, diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 50bff935e32f..96dcce0290ea 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -7,90 +7,66 @@ -include("src/rabbit_cli_backend.hrl"). --export([final_argparse_def/0, run_command/1]). +-export([run_command/1]). + +%% TODO: +%% * Implémenter "list exchanges" plus proprement +%% * Implémenter "rabbitmqctl list_exchanges" pour la compatibilité + +run_command(ContextMap) when is_map(ContextMap) -> + Context = map_to_context(ContextMap), + run_command(Context); +run_command(#rabbit_cli{} = Context) -> + maybe + %% We can query the argparse definition from the remote node to know + %% the commands it supports and proceed with the execution. + ArgparseDef = final_argparse_def(Context), + Context1 = Context#rabbit_cli{argparse_def = ArgparseDef}, + + {ok, ArgMap, CmdPath, Command} ?= final_parse(Context1), + Context2 = Context1#rabbit_cli{arg_map = ArgMap, + cmd_path = CmdPath, + command = Command}, + + do_run_command(Context2) + end. %% ------------------------------------------------------------------- -%% Commands discovery. +%% Argparse definition handling. %% ------------------------------------------------------------------- -final_argparse_def() -> - #{argparse_def := ArgparseDef} = get_discovered_commands(), - ArgparseDef. - -get_discovered_commands() -> - Key = {?MODULE, discovered_commands}, - try - persistent_term:get(Key) - catch - error:badarg -> - Commands = discover_commands(), - ArgparseDef = commands_to_cli_argparse_def(Commands), - Cache = #{commands => Commands, - argparse_def => ArgparseDef}, - persistent_term:put(Key, Cache), - Cache - end. +final_argparse_def( + #rabbit_cli{argparse_def = PartialArgparseDef}) -> + FullArgparseDef = rabbit_cli_commands:discovered_argparse_def(), + ArgparseDef1 = merge_argparse_def(PartialArgparseDef, FullArgparseDef), + ArgparseDef1. + +merge_argparse_def(ArgparseDef1, ArgparseDef2) -> + Args1 = maps:get(arguments, ArgparseDef1, []), + Args2 = maps:get(arguments, ArgparseDef2, []), + Args = merge_arguments(Args1, Args2), + Cmds1 = maps:get(commands, ArgparseDef1, #{}), + Cmds2 = maps:get(commands, ArgparseDef2, #{}), + Cmds = merge_commands(Cmds1, Cmds2), + maps:merge( + ArgparseDef1, + ArgparseDef2#{arguments => Args, commands => Cmds}). + +merge_arguments(Args1, Args2) -> + Args1 ++ Args2. -discover_commands() -> - %% Extract the commands from module attributes like feature flags and boot - %% steps. - ?LOG_DEBUG( - "Commands: query commands in loaded applications", - #{domain => ?RMQLOG_DOMAIN_CMD}), - T0 = erlang:monotonic_time(), - AttrsPerApp = rabbit_misc:rabbitmq_related_module_attributes( - rabbitmq_command), - T1 = erlang:monotonic_time(), - ?LOG_DEBUG( - "Commands: time to find supported commands: ~tp us", - [erlang:convert_time_unit(T1 - T0, native, microsecond)], - #{domain => ?RMQLOG_DOMAIN_CMD}), - AttrsPerApp. - -commands_to_cli_argparse_def(Commands) -> - lists:foldl( - fun({_App, _Mod, Entries}, Acc0) -> - lists:foldl( - fun - ({#{cli := Path}, Def}, Acc1) -> - Def1 = expand_argparse_def(Def), - M1 = lists:foldr( - fun - (Cmd, undefined) -> - #{commands => #{Cmd => Def1}}; - (Cmd, M0) -> - #{commands => #{Cmd => M0}} - end, undefined, Path), - rabbit_cli:merge_argparse_def(Acc1, M1); - (_, Acc1) -> - Acc1 - end, Acc0, Entries) - end, #{}, Commands). - -expand_argparse_def(Def) when is_map(Def) -> - Def; -expand_argparse_def(Defs) when is_list(Defs) -> - lists:foldl( - fun - (argparse_def_record_stream, Acc) -> - Def = rabbit_cli_io:argparse_def(record_stream), - rabbit_cli:merge_argparse_def(Acc, Def); - (argparse_def_file_input, Acc) -> - Def = rabbit_cli_io:argparse_def(file_input), - rabbit_cli:merge_argparse_def(Acc, Def); - (Def, Acc) -> - Def1 = expand_argparse_def(Def), - rabbit_cli:merge_argparse_def(Acc, Def1) - end, #{}, Defs). +merge_commands(Cmds1, Cmds2) -> + maps:merge(Cmds1, Cmds2). + +final_parse( + #rabbit_cli{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> + Options = #{progname => ProgName}, + argparse:parse(Args, ArgparseDef, Options). %% ------------------------------------------------------------------- -%% Commands execution. +%% Command execution. %% ------------------------------------------------------------------- -run_command(ContextMap) -> - Context = map_to_context(ContextMap), - do_run_command(Context). - do_run_command( #rabbit_cli{command = #{handler := {Module, Function}}} = Context) -> erlang:apply(Module, Function, [Context]). diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 51ae6973df6d..7e95eb325e49 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -5,7 +5,9 @@ -include_lib("rabbit_common/include/logging.hrl"). -include_lib("rabbit_common/include/resource.hrl"). --export([argparse_def/0, run_command/1, do_run_command/1]). +-export([discover_commands/0, + discovered_commands/0, + discovered_argparse_def/0]). -export([cmd_list_exchanges/1, cmd_import_definitions/1, cmd_top/1]). @@ -44,17 +46,21 @@ [#{help => "Top-like interactive view", handler => {?MODULE, cmd_top}}]}). -argparse_def() -> - #{argparse_def := ArgparseDef} = get_discovered_commands(), - ArgparseDef. +%% ------------------------------------------------------------------- +%% Commands discovery. +%% ------------------------------------------------------------------- + +discover_commands() -> + _ = discovered_commands_and_argparse_def(), + ok. -get_discovered_commands() -> +discovered_commands_and_argparse_def() -> Key = {?MODULE, discovered_commands}, try persistent_term:get(Key) catch error:badarg -> - Commands = discover_commands(), + Commands = do_discover_commands(), ArgparseDef = commands_to_cli_argparse_def(Commands), Cache = #{commands => Commands, argparse_def => ArgparseDef}, @@ -62,16 +68,26 @@ get_discovered_commands() -> Cache end. -discover_commands() -> +discovered_commands() -> + #{commands := Commands} = discovered_commands_and_argparse_def(), + Commands. + +discovered_argparse_def() -> + #{argparse_def := ArgparseDef} = discovered_commands_and_argparse_def(), + ArgparseDef. + +do_discover_commands() -> %% Extract the commands from module attributes like feature flags and boot %% steps. + %% TODO: Discover commands as a boot step. + %% TODO: Write shell completion scripts for various shells as part of that. + %% TODO: Generate manpages? When/how? With eDoc? ?LOG_DEBUG( "Commands: query commands in loaded applications", #{domain => ?RMQLOG_DOMAIN_CMD}), T0 = erlang:monotonic_time(), - ScannedApps = rabbit_misc:rabbitmq_related_apps(), - AttrsPerApp = rabbit_misc:module_attributes_from_apps( - rabbitmq_command, ScannedApps), + AttrsPerApp = rabbit_misc:rabbitmq_related_module_attributes( + rabbitmq_command), T1 = erlang:monotonic_time(), ?LOG_DEBUG( "Commands: time to find supported commands: ~tp us", @@ -115,32 +131,9 @@ expand_argparse_def(Defs) when is_list(Defs) -> rabbit_cli:merge_argparse_def(Acc, Def1) end, #{}, Defs). -run_command(Context) -> - %% TODO: Put both processes under the rabbit supervision tree. - Parent = self(), - RunnerPid = spawn_link( - fun() -> - Ret = do_run_command(Context), - case Ret of - ok -> - ok; - {ok, _} -> - ok; - Other -> - erlang:unlink(Parent), - erlang:error(Other) - end - end), - RunnerMRef = erlang:monitor(process, RunnerPid), - receive - {'DOWN', RunnerMRef, _, _, normal} -> - ok; - {'DOWN', RunnerMRef, _, _, Reason} -> - Reason - end. - -do_run_command(#{command := #{handler := {Mod, Fun}}} = Context) -> - erlang:apply(Mod, Fun, [Context]). +%% ------------------------------------------------------------------- +%% XXX +%% ------------------------------------------------------------------- cmd_list_exchanges(#{progname := Progname, arg_map := ArgMap}) -> logger:alert("CLI: running list exchanges"), From c58ba79456afd18925b270af9a640ae369dfd641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 18 Jun 2025 11:41:57 +0200 Subject: [PATCH 23/51] Setup logging in CLI frontend --- deps/rabbit/src/rabbit_cli_frontend.erl | 167 +++++++++------------- deps/rabbit/src/rabbit_cli_transport2.erl | 12 +- 2 files changed, 82 insertions(+), 97 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index e584dbcb7722..921003d7f31f 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -4,7 +4,6 @@ -include_lib("stdlib/include/assert.hrl"). -export([main/1, - merge_argparse_def/2, noop/1]). -record(?MODULE, {scriptname, @@ -19,20 +18,18 @@ main(Args) -> ScriptName = escript:script_name(), + add_rabbitmq_code_path(ScriptName), + configure_logging(), + Ret = run_cli(ScriptName, Args), - io:format(standard_error, "CLI run_cli() -> ~p~n", [Ret]), + ?LOG_NOTICE("CLI: run_cli() return value: ~p", [Ret]), erlang:halt(). -run_cli(ScriptName, Args) -> - ProgName = filename:basename(ScriptName, ".escript"), - GroupLeader = erlang:group_leader(), - Context = #?MODULE{scriptname = ScriptName, - progname = ProgName, - args = Args, - group_leader = GroupLeader}, - add_rabbitmq_code_path(Context). +%% ------------------------------------------------------------------- +%% CLI frontend setup. +%% ------------------------------------------------------------------- -add_rabbitmq_code_path(#?MODULE{scriptname = ScriptName} = Context) -> +add_rabbitmq_code_path(ScriptName) -> ScriptDir = filename:dirname(ScriptName), PluginsDir0 = filename:join([ScriptDir, "..", "plugins"]), PluginsDir1 = case filelib:is_dir(PluginsDir0) of @@ -41,12 +38,36 @@ add_rabbitmq_code_path(#?MODULE{scriptname = ScriptName} = Context) -> end, Glob = filename:join([PluginsDir1, "*", "ebin"]), AppDirs = filelib:wildcard(Glob), - lists:foreach(fun code:add_path/1, AppDirs), + lists:foreach(fun code:add_path/1, AppDirs). + +configure_logging() -> + Config = #{level => debug, + config => #{type => standard_error}, + filters => [{progress_reports, + {fun logger_filters:progress/2, stop}}], + formatter => {rabbit_logger_text_fmt, + #{single_line => false, + use_colors => true}}}, + ok = logger:add_handler(rmq_cli, rabbit_logger_std_h, Config), + ok = logger:remove_handler(default), + ok. + +%% ------------------------------------------------------------------- +%% Preparation for remote command execution. +%% ------------------------------------------------------------------- + +run_cli(ScriptName, Args) -> + ProgName = filename:basename(ScriptName, ".escript"), + GroupLeader = erlang:group_leader(), + Context = #?MODULE{scriptname = ScriptName, + progname = ProgName, + args = Args, + group_leader = GroupLeader}, init_local_args(Context). init_local_args(Context) -> maybe - LocalArgparseDef = local_argparse_def(), + LocalArgparseDef = initial_argparse_def(), Context1 = Context#?MODULE{argparse_def = LocalArgparseDef}, {ok, @@ -56,53 +77,36 @@ init_local_args(Context) -> Context2 = Context1#?MODULE{arg_map = PartialArgMap, cmd_path = PartialCmdPath, command = PartialCommand}, - connect_to_node(Context2) - end. - -connect_to_node(#?MODULE{arg_map = #{node := NodenameOrUri}} = Context) -> - maybe - %% TODO: Send a list of supported features: - %% * support for some messages, like Erlang I/O protocol, file - %% read/write support - %% * type of terminal (or no terminal) - %% * capabilities of the terminal - %% * is plain test or HTTP - %% * evolutions in the communication between the frontend and the - %% backend - {ok, Connection} ?= rabbit_cli_transport2:connect(NodenameOrUri), - Context1 = Context#?MODULE{connection = Connection}, - init_final_args(Context1) - end; -connect_to_node(#?MODULE{} = Context) -> - maybe - {ok, Connection} ?= rabbit_cli_transport2:connect(), - Context1 = Context#?MODULE{connection = Connection}, - init_final_args(Context1) + set_log_level(Context2) end. -init_final_args(Context) -> - maybe - %% We can query the argparse definition from the remote node to know - %% the commands it supports and proceed with the execution. - {ok, ArgparseDef} ?= final_argparse_def(Context), - Context1 = Context#?MODULE{argparse_def = ArgparseDef}, - - {ok, - ArgMap, - CmdPath, - Command} ?= final_parse(Context1), - Context2 = Context1#?MODULE{arg_map = ArgMap, - cmd_path = CmdPath, - command = Command}, - - run_command(Context2) - end. +set_log_level(#?MODULE{arg_map = #{verbose := Verbosity}} = Context) + when Verbosity >= 3 -> + logger:set_primary_config(level, debug), + connect_to_node(Context); +set_log_level(#?MODULE{} = Context) -> + connect_to_node(Context). + +connect_to_node(#?MODULE{arg_map = ArgMap} = Context) -> + Ret = case ArgMap of + #{node := NodenameOrUri} -> + rabbit_cli_transport2:connect(NodenameOrUri); + _ -> + rabbit_cli_transport2:connect() + end, + Context1 = case Ret of + {ok, Connection} -> + Context#?MODULE{connection = Connection}; + {error, _Reason} -> + Context#?MODULE{connection = none} + end, + run_command(Context1). %% ------------------------------------------------------------------- %% Arguments definition and parsing. %% ------------------------------------------------------------------- -local_argparse_def() -> +initial_argparse_def() -> #{arguments => [ #{name => help, @@ -158,57 +162,28 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> Error end. -final_argparse_def(#?MODULE{argparse_def = PartialArgparseDef} = Context) -> - maybe - {ok, FullArgparseDef} ?= get_full_argparse_def(Context), - ArgparseDef1 = merge_argparse_def(PartialArgparseDef, FullArgparseDef), - {ok, ArgparseDef1} - end. - -get_full_argparse_def(#?MODULE{connection = Connection}) - when Connection =/= undefined -> - %% TODO: Handle an undef failure when the remote node is too old. - RemoteArgparseDef = rabbit_cli_transport2:rpc( - Connection, - rabbit_cli_backend, final_argparse_def, []), - {ok, RemoteArgparseDef}; -get_full_argparse_def(#?MODULE{}) -> - %% FIXME: Load applications first, otherwise module attributes are - %% unavailable. - LocalArgparseDef = rabbit_cli_backend:final_argparse_def(), - {ok, LocalArgparseDef}. - -merge_argparse_def(ArgparseDef1, ArgparseDef2) -> - Args1 = maps:get(arguments, ArgparseDef1, []), - Args2 = maps:get(arguments, ArgparseDef2, []), - Args = merge_arguments(Args1, Args2), - Cmds1 = maps:get(commands, ArgparseDef1, #{}), - Cmds2 = maps:get(commands, ArgparseDef2, #{}), - Cmds = merge_commands(Cmds1, Cmds2), - maps:merge( - ArgparseDef1, - ArgparseDef2#{arguments => Args, commands => Cmds}). - -merge_arguments(Args1, Args2) -> - Args1 ++ Args2. - -merge_commands(Cmds1, Cmds2) -> - maps:merge(Cmds1, Cmds2). - -final_parse( - #?MODULE{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> - Options = #{progname => ProgName}, - argparse:parse(Args, ArgparseDef, Options). - %% ------------------------------------------------------------------- %% Command execution. %% ------------------------------------------------------------------- -run_command(#?MODULE{connection = Connection} = Context) -> +%% TODO: Send a list of supported features: +%% * support for some messages, like Erlang I/O protocol, file read/write +%% support +%% * type of terminal (or no terminal) +%% * capabilities of the terminal +%% * is plain test or HTTP +%% * evolutions in the communication between the frontend and the backend + +run_command(#?MODULE{connection = Connection} = Context) + when Connection =/= none -> ContextMap = context_to_map(Context), rabbit_cli_transport2:rpc( Connection, rabbit_cli_backend, run_command, [ContextMap]); run_command(Context) -> + %% TODO: If we can't connect to a node, try to parse args locally and run + %% the command on this CLI node. + %% FIXME: Load applications first, otherwise module attributes are + %% unavailable. %% FIXME: Do we need to spawn a process? ContextMap = context_to_map(Context), rabbit_cli_backend:run_command(ContextMap). diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index f1f6e635e48a..3eea66f3bcb4 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -1,5 +1,7 @@ -module(rabbit_cli_transport2). +-include_lib("kernel/include/logger.hrl"). + -export([connect/0, connect/1, rpc/4, send/3]). @@ -18,6 +20,11 @@ connect(NodenameOrUri) -> connect(erldist = Proto, Nodename) -> maybe Nodename1 = complete_nodename(Nodename), + ?LOG_DEBUG( + "CLI: connect to node ~s using Erlang distribution", + [Nodename1]), + + %% FIXME: Handle short vs. long names. {ok, _} ?= net_kernel:start(undefined, #{name_domain => shortnames}), %% Can we reach the remote node? @@ -32,7 +39,10 @@ connect(erldist = Proto, Nodename) -> end; connect(http = Proto, Uri) -> maybe - {ok, Client} = rabbit_cli_http_client:start_link(Uri), + ?LOG_DEBUG( + "CLI: connect to URI ~s using HTTP", + [Uri]), + {ok, Client} ?= rabbit_cli_http_client:start_link(Uri), Connection = #?MODULE{type = Proto, peer = Client}, {ok, Connection} From f8331a8ccc020cde1cfe8f50046d9e741b2849bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 18 Jun 2025 13:03:15 +0200 Subject: [PATCH 24/51] Add "hello" command --- deps/rabbit/src/rabbit_cli_commands.erl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 7e95eb325e49..f15a4230fd5c 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -8,10 +8,16 @@ -export([discover_commands/0, discovered_commands/0, discovered_argparse_def/0]). --export([cmd_list_exchanges/1, +-export([cmd_hello/1, + cmd_list_exchanges/1, cmd_import_definitions/1, cmd_top/1]). +-rabbitmq_command( + {#{cli => ["hello"]}, + #{help => "Say hello!", + handler => {?MODULE, cmd_hello}}}). + -rabbitmq_command( {#{cli => ["declare", "exchange"], http => {put, ["exchanges", vhost, exchange]}}, @@ -135,6 +141,11 @@ expand_argparse_def(Defs) when is_list(Defs) -> %% XXX %% ------------------------------------------------------------------- +cmd_hello(_) -> + Name = io:get_line("Name: "), + io:format("Hello ~s!~n", [string:trim(Name)]), + ok. + cmd_list_exchanges(#{progname := Progname, arg_map := ArgMap}) -> logger:alert("CLI: running list exchanges"), InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], From ba30cd4e753826d871bdb5fa724dd13c03052a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 18 Jun 2025 17:09:36 +0200 Subject: [PATCH 25/51] Mention flush of stdout/stderr --- deps/rabbit/src/rabbit_cli_frontend.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 921003d7f31f..d2d1b94e058b 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -23,6 +23,7 @@ main(Args) -> Ret = run_cli(ScriptName, Args), ?LOG_NOTICE("CLI: run_cli() return value: ~p", [Ret]), + %% FIXME: Ensures everything written to stdout/stderr was flushed. erlang:halt(). %% ------------------------------------------------------------------- From d83d95401ba734def35a6e55c1b996093a998c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 19 Jun 2025 11:48:03 +0200 Subject: [PATCH 26/51] Put rabbit_cli_backend under supervisor --- deps/rabbit/src/rabbit.erl | 6 ++ deps/rabbit/src/rabbit_cli_backend.erl | 92 +++++++++++----- deps/rabbit/src/rabbit_cli_backend.hrl | 7 +- deps/rabbit/src/rabbit_cli_backend_sup.erl | 20 ++++ deps/rabbit/src/rabbit_cli_commands.erl | 20 +++- deps/rabbit/src/rabbit_cli_frontend.erl | 104 ++++++++++++------- deps/rabbit/src/rabbit_cli_http_client.erl | 21 +++- deps/rabbit/src/rabbit_cli_http_listener.erl | 92 ++++++++-------- deps/rabbit/src/rabbit_cli_transport2.erl | 21 +++- 9 files changed, 259 insertions(+), 124 deletions(-) create mode 100644 deps/rabbit/src/rabbit_cli_backend_sup.erl diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index a5780fc4e81d..1c968329916f 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -236,6 +236,12 @@ {enables, routing_ready}]}). %% CLI-related boot steps. +-rabbit_boot_step({rabbit_cli_backend_sup, + [{description, "RabbitMQ CLI command supervisor"}, + {mfa, {rabbit_sup, start_supervisor_child, + [rabbit_cli_backend_sup]}}, + {requires, [core_initialized, recovery]}, + {enables, routing_ready}]}). -rabbit_boot_step({rabbit_cli_command_discovery, [{description, "RabbitMQ CLI command discovery"}, {mfa, {rabbit_cli_commands, discover_commands, diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 96dcce0290ea..4ffc76583f37 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -1,5 +1,7 @@ -module(rabbit_cli_backend). +-behaviour(gen_statem). + -include_lib("kernel/include/logger.hrl"). -include_lib("rabbit_common/include/logging.hrl"). @@ -7,29 +9,76 @@ -include("src/rabbit_cli_backend.hrl"). --export([run_command/1]). +-export([run_command/2, + start_link/3]). +-export([init/1, + callback_mode/0, + handle_event/4, + terminate/3, + code_change/4]). %% TODO: %% * Implémenter "list exchanges" plus proprement %% * Implémenter "rabbitmqctl list_exchanges" pour la compatibilité -run_command(ContextMap) when is_map(ContextMap) -> +run_command(ContextMap, Caller) when is_map(ContextMap) -> Context = map_to_context(ContextMap), - run_command(Context); -run_command(#rabbit_cli{} = Context) -> - maybe - %% We can query the argparse definition from the remote node to know - %% the commands it supports and proceed with the execution. - ArgparseDef = final_argparse_def(Context), - Context1 = Context#rabbit_cli{argparse_def = ArgparseDef}, - - {ok, ArgMap, CmdPath, Command} ?= final_parse(Context1), - Context2 = Context1#rabbit_cli{arg_map = ArgMap, - cmd_path = CmdPath, - command = Command}, - - do_run_command(Context2) - end. + run_command(Context, Caller); +run_command(#rabbit_cli{} = Context, Caller) when is_pid(Caller) -> + GroupLeader = erlang:group_leader(), + rabbit_cli_backend_sup:start_backend(Context, Caller, GroupLeader). + +map_to_context(ContextMap) -> + #rabbit_cli{progname = maps:get(progname, ContextMap), + args = maps:get(args, ContextMap), + argparse_def = maps:get(argparse_def, ContextMap), + arg_map = maps:get(arg_map, ContextMap), + cmd_path = maps:get(cmd_path, ContextMap), + command = maps:get(command, ContextMap), + + frontend_priv = undefined}. + +start_link(Context, Caller, GroupLeader) -> + Args = #{context => Context, + caller => Caller, + group_leader => GroupLeader}, + gen_statem:start_link(?MODULE, Args, []). + +init(#{context := Context, caller := Caller, group_leader := GroupLeader}) -> + process_flag(trap_exit, true), + erlang:link(Caller), + erlang:group_leader(GroupLeader, self()), + {ok, standing_by, Context, {next_event, internal, parse_command}}. + +callback_mode() -> + handle_event_function. + +handle_event(internal, parse_command, standing_by, Context) -> + %% We can query the argparse definition from the remote node to know + %% the commands it supports and proceed with the execution. + ArgparseDef = final_argparse_def(Context), + Context1 = Context#rabbit_cli{argparse_def = ArgparseDef}, + + case final_parse(Context1) of + {ok, ArgMap, CmdPath, Command} -> + Context2 = Context1#rabbit_cli{arg_map = ArgMap, + cmd_path = CmdPath, + command = Command}, + {next_state, command_parsed, Context2, + {next_event, internal, run_command}}; + {error, Reason} -> + {stop, {failed_to_parse_command, Reason}} + end; +handle_event(internal, run_command, command_parsed, Context) -> + Ret = do_run_command(Context), + {stop, {shutdown, Ret}, Context}. + +terminate(Reason, _State, _Data) -> + ?LOG_DEBUG("CLI: backend terminating: ~0p", [Reason]), + ok. + +code_change(_Vsn, State, Data, _Extra) -> + {ok, State, Data}. %% ------------------------------------------------------------------- %% Argparse definition handling. @@ -70,12 +119,3 @@ final_parse( do_run_command( #rabbit_cli{command = #{handler := {Module, Function}}} = Context) -> erlang:apply(Module, Function, [Context]). - -map_to_context(ContextMap) -> - #rabbit_cli{scriptname = maps:get(scriptname, ContextMap), - progname = maps:get(progname, ContextMap), - args = maps:get(args, ContextMap), - argparse_def = maps:get(argparse_def, ContextMap), - arg_map = maps:get(arg_map, ContextMap), - cmd_path = maps:get(cmd_path, ContextMap), - command = maps:get(command, ContextMap)}. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index 9a4b6e762648..e0df9c7077e2 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -1,7 +1,8 @@ --record(rabbit_cli, {scriptname, - progname, +-record(rabbit_cli, {progname, args, argparse_def, arg_map, cmd_path, - command}). + command, + + frontend_priv}). diff --git a/deps/rabbit/src/rabbit_cli_backend_sup.erl b/deps/rabbit/src/rabbit_cli_backend_sup.erl new file mode 100644 index 000000000000..e8c73b067663 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_backend_sup.erl @@ -0,0 +1,20 @@ +-module(rabbit_cli_backend_sup). + +-behaviour(supervisor). + +-export([start_link/0, + start_backend/3]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, none). + +start_backend(Context, Caller, GroupLeader) -> + supervisor:start_child(?MODULE, [Context, Caller, GroupLeader]). + +init(_Args) -> + SupFlags = #{strategy => simple_one_for_one}, + BackendChild = #{id => rabbit_cli_backend, + start => {rabbit_cli_backend, start_link, []}, + restart => temporary}, + {ok, {SupFlags, [BackendChild]}}. diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index f15a4230fd5c..a0967dcda901 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -8,16 +8,28 @@ -export([discover_commands/0, discovered_commands/0, discovered_argparse_def/0]). --export([cmd_hello/1, +-export([cmd_noop/1, + cmd_hello/1, + cmd_crash/1, cmd_list_exchanges/1, cmd_import_definitions/1, cmd_top/1]). +-rabbitmq_command( + {#{cli => ["noop"]}, + #{help => "No-op", + handler => {?MODULE, cmd_noop}}}). + -rabbitmq_command( {#{cli => ["hello"]}, #{help => "Say hello!", handler => {?MODULE, cmd_hello}}}). +-rabbitmq_command( + {#{cli => ["crash"]}, + #{help => "Crash", + handler => {?MODULE, cmd_crash}}}). + -rabbitmq_command( {#{cli => ["declare", "exchange"], http => {put, ["exchanges", vhost, exchange]}}, @@ -141,11 +153,17 @@ expand_argparse_def(Defs) when is_list(Defs) -> %% XXX %% ------------------------------------------------------------------- +cmd_noop(_) -> + ok. + cmd_hello(_) -> Name = io:get_line("Name: "), io:format("Hello ~s!~n", [string:trim(Name)]), ok. +cmd_crash(_) -> + erlang:exit(oops). + cmd_list_exchanges(#{progname := Progname, arg_map := ArgMap}) -> logger:alert("CLI: running list exchanges"), InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index d2d1b94e058b..babd9a7c889d 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -3,17 +3,12 @@ -include_lib("kernel/include/logger.hrl"). -include_lib("stdlib/include/assert.hrl"). +-include("src/rabbit_cli_backend.hrl"). + -export([main/1, noop/1]). -record(?MODULE, {scriptname, - progname, - group_leader, - args, - argparse_def, - arg_map, - cmd_path, - command, connection}). main(Args) -> @@ -24,6 +19,7 @@ main(Args) -> Ret = run_cli(ScriptName, Args), ?LOG_NOTICE("CLI: run_cli() return value: ~p", [Ret]), %% FIXME: Ensures everything written to stdout/stderr was flushed. + timer:sleep(50), erlang:halt(). %% ------------------------------------------------------------------- @@ -59,48 +55,49 @@ configure_logging() -> run_cli(ScriptName, Args) -> ProgName = filename:basename(ScriptName, ".escript"), - GroupLeader = erlang:group_leader(), - Context = #?MODULE{scriptname = ScriptName, - progname = ProgName, - args = Args, - group_leader = GroupLeader}, + Priv = #?MODULE{scriptname = ScriptName}, + Context = #rabbit_cli{progname = ProgName, + args = Args, + frontend_priv = Priv}, init_local_args(Context). init_local_args(Context) -> maybe LocalArgparseDef = initial_argparse_def(), - Context1 = Context#?MODULE{argparse_def = LocalArgparseDef}, + Context1 = Context#rabbit_cli{argparse_def = LocalArgparseDef}, {ok, PartialArgMap, PartialCmdPath, PartialCommand} ?= initial_parse(Context1), - Context2 = Context1#?MODULE{arg_map = PartialArgMap, - cmd_path = PartialCmdPath, - command = PartialCommand}, + Context2 = Context1#rabbit_cli{arg_map = PartialArgMap, + cmd_path = PartialCmdPath, + command = PartialCommand}, set_log_level(Context2) end. -set_log_level(#?MODULE{arg_map = #{verbose := Verbosity}} = Context) +set_log_level(#rabbit_cli{arg_map = #{verbose := Verbosity}} = Context) when Verbosity >= 3 -> logger:set_primary_config(level, debug), connect_to_node(Context); -set_log_level(#?MODULE{} = Context) -> +set_log_level(#rabbit_cli{} = Context) -> connect_to_node(Context). -connect_to_node(#?MODULE{arg_map = ArgMap} = Context) -> +connect_to_node( + #rabbit_cli{arg_map = ArgMap, frontend_priv = Priv} = Context) -> Ret = case ArgMap of #{node := NodenameOrUri} -> rabbit_cli_transport2:connect(NodenameOrUri); _ -> rabbit_cli_transport2:connect() end, - Context1 = case Ret of - {ok, Connection} -> - Context#?MODULE{connection = Connection}; - {error, _Reason} -> - Context#?MODULE{connection = none} - end, + Priv1 = case Ret of + {ok, Connection} -> + Priv#?MODULE{connection = Connection}; + {error, _Reason} -> + Priv#?MODULE{connection = none} + end, + Context1 = Context#rabbit_cli{frontend_priv = Priv1}, run_command(Context1). %% ------------------------------------------------------------------- @@ -138,7 +135,7 @@ initial_argparse_def() -> handler => {?MODULE, noop}}. initial_parse( - #?MODULE{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> + #rabbit_cli{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> Options = #{progname => ProgName}, case partial_parse(Args, ArgparseDef, Options) of {ok, ArgMap, CmdPath, Command, _RemainingArgs} -> @@ -163,10 +160,18 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> Error end. +noop(_Context) -> + ok. + %% ------------------------------------------------------------------- %% Command execution. %% ------------------------------------------------------------------- +%% Run command: +%% * start backend (remote if connection, local otherwise); backend starts +%% execution of command +%% * loop to react to signals and messages from backend +%% %% TODO: Send a list of supported features: %% * support for some messages, like Erlang I/O protocol, file read/write %% support @@ -175,26 +180,34 @@ partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> %% * is plain test or HTTP %% * evolutions in the communication between the frontend and the backend -run_command(#?MODULE{connection = Connection} = Context) +run_command( + #rabbit_cli{frontend_priv = #?MODULE{connection = Connection}} = Context) when Connection =/= none -> - ContextMap = context_to_map(Context), - rabbit_cli_transport2:rpc( - Connection, rabbit_cli_backend, run_command, [ContextMap]); -run_command(Context) -> + maybe + process_flag(trap_exit, true), + ContextMap = context_to_map(Context), + {ok, _Backend} ?= rabbit_cli_transport2:run_command( + Connection, ContextMap), + main_loop(Context) + end; +run_command(#rabbit_cli{} = Context) -> %% TODO: If we can't connect to a node, try to parse args locally and run %% the command on this CLI node. %% FIXME: Load applications first, otherwise module attributes are %% unavailable. - %% FIXME: Do we need to spawn a process? - ContextMap = context_to_map(Context), - rabbit_cli_backend:run_command(ContextMap). + %% FIXME: run_command() relies on rabbit_cli_backend_sup. + maybe + process_flag(trap_exit, true), + ContextMap = context_to_map(Context), + {ok, _Backend} ?= rabbit_cli_backend:run_command(ContextMap), + main_loop(Context) + end. context_to_map(Context) -> - Fields = [Field || Field <- record_info(fields, ?MODULE), - %% We don’t need or want to communicate the connection - %% state or the group leader to the backend. - Field =/= connection orelse - Field =/= group_leader], + Fields = [Field || Field <- record_info(fields, rabbit_cli), + %% We don't need or want to communicate anything that + %% is private to the frontend. + Field =/= frontend_priv], record_to_map(Fields, Context, 2, #{}). record_to_map([Field | Rest], Record, Index, Map) -> @@ -204,5 +217,16 @@ record_to_map([Field | Rest], Record, Index, Map) -> record_to_map([], _Record, _Index, Map) -> Map. -noop(_Context) -> +main_loop(#rabbit_cli{} = Context) -> + ?LOG_DEBUG("CLI: frontend main loop..."), + receive + {'EXIT', _LinkedPid, Reason} -> + terminate(Reason, Context); + Info -> + ?LOG_DEBUG("Unknown info: ~0p", [Info]), + main_loop(Context) + end. + +terminate(Reason, _Context) -> + ?LOG_DEBUG("CLI: frontend terminating: ~0p", [Reason]), ok. diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index c37caac0d921..5c10e22ee589 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -5,7 +5,9 @@ -include_lib("kernel/include/logger.hrl"). -export([start_link/1, t/0, + run_command/2, rpc/4, + link/2, send/3]). -export([init/1, callback_mode/0, @@ -28,9 +30,15 @@ t() -> start_link(Uri) -> gen_statem:start_link(?MODULE, Uri, []). +run_command(Client, ContextMap) -> + gen_statem:call(Client, {?FUNCTION_NAME, ContextMap}). + rpc(Client, Module, Function, Args) -> gen_statem:call(Client, {?FUNCTION_NAME, Module, Function, Args}). +link(Client, Pid) -> + gen_statem:call(Client, {?FUNCTION_NAME, Pid}). + send(Client, Dest, Msg) -> gen_statem:cast(Client, {?FUNCTION_NAME, Dest, Msg}). @@ -103,13 +111,15 @@ handle_event( stream_ready, #?MODULE{} = Data) -> Request = binary_to_term(RequestBin), - ?LOG_DEBUG("CLI: received request from server: ~p", [Request]), + ?LOG_DEBUG("CLI: received HTTP message from server: ~p", [Request]), case handle_request(Request, Data) of {reply, Reply, Data1} -> send_request(Reply, Data1), {keep_state, Data1}; {noreply, Data1} -> - {keep_state, Data1} + {keep_state, Data1}; + {stop, Reason} -> + {stop, Reason, Data} end; handle_event( info, {io_reply, ProxyRef, Reply}, @@ -138,7 +148,8 @@ handle_event( %% FIXME: Handle pending requests. {stop, normal, Data}. -terminate(_Reason, _State, _Data) -> +terminate(Reason, _State, _Data) -> + ?LOG_DEBUG("CLI: HTTP client terminating: ~0p", [Reason]), ok. code_change(_Vsn, State, Data, _Extra) -> @@ -183,4 +194,6 @@ handle_request( GroupLeader ! ProxyIoRequest, IoRequests1 = IoRequests#{ProxyRef => {From, ReplyAs}}, Data1 = Data#?MODULE{io_requests = IoRequests1}, - {noreply, Data1}. + {noreply, Data1}; +handle_request({'EXIT', _Pid, Reason}, _Data) -> + {stop, Reason}. diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl index 4d91ec515674..96818c2b399f 100644 --- a/deps/rabbit/src/rabbit_cli_http_listener.erl +++ b/deps/rabbit/src/rabbit_cli_http_listener.erl @@ -107,7 +107,6 @@ config_change(_OldVsn, State, _Extra) -> %% ------------------------------------------------------------------- init(#{method := <<"GET">>} = Req, State) -> - ?LOG_DEBUG("CLI: received HTTP request: ~p", [Req]), UpgradeHeader = cowboy_req:header(<<"upgrade">>, Req), case UpgradeHeader of <<"websocket">> -> @@ -124,25 +123,33 @@ init(#{method := <<"GET">>} = Req, State) -> end end; init(Req, State) -> - ?LOG_DEBUG("CLI: received HTTP request: ~p", [Req]), Req1 = reply_with_help(Req, 405), {ok, Req1, State}. websocket_init(State) -> - {ok, Server} = rabbit_cli_http_server:start_link(self()), - State1 = State#{server => Server, - reqids => gen_server:reqids_new()}, - {ok, State1}. - -websocket_handle( - {binary, RequestBin}, - #{server := Server, reqids := ReqIds} = State) -> + process_flag(trap_exit, true), + erlang:group_leader(self(), self()), + {ok, State}. + +websocket_handle({binary, RequestBin}, State) -> Request = binary_to_term(RequestBin), - ?LOG_DEBUG("CLI: received request from client: ~p", [Request]), - ReqIds1 = rabbit_cli_http_server:send_request( - Server, Request, undefined, ReqIds), - State1 = State#{reqids => ReqIds1}, - {ok, State1}; + ?LOG_DEBUG("CLI: received HTTP message from client: ~p", [Request]), + try + case handle_request(Request) of + {reply, Reply} -> + ReplyBin = term_to_binary(Reply), + Frame1 = {binary, ReplyBin}, + {[Frame1], State}; + noreply -> + {ok, State} + end + catch + Class:Reason:Stacktrace -> + Exception = {call_exception, Class, Reason, Stacktrace}, + ExceptionBin = term_to_binary(Exception), + Frame2 = {binary, ExceptionBin}, + {[Frame2], State} + end; websocket_handle(Frame, State) -> ?LOG_DEBUG("CLI: unhandled Websocket frame: ~p", [Frame]), {ok, State}. @@ -151,38 +158,13 @@ websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> IoRequestBin = term_to_binary(IoRequest), Frame = {binary, IoRequestBin}, {[Frame], State}; -websocket_info(Info, #{server := Server, reqids := ReqIds} = State) -> - case gen_server:check_response(Info, ReqIds, true) of - {{reply, Response}, _Label, ReqIds1} -> - State1 = State#{reqids => ReqIds1}, - case Response of - {reply, Reply} -> - ReplyBin = term_to_binary(Reply), - Frame = {binary, ReplyBin}, - {[Frame], State1}; - noreply -> - {ok, State1} - end; - {{error, {Reason, Server}}, _Label, ReqIds1} -> - State1 = State#{reqids => ReqIds1}, - ?LOG_DEBUG("CLI: error from gen_server request: ~p", [Reason]), - {ok, State1}; - NotResponse - when NotResponse =:= no_request orelse NotResponse =:= no_reply -> - ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), - {ok, State} - end. +websocket_info({'EXIT', _Pid, _Reason} = Exit, State) -> + ExitBin = term_to_binary(Exit), + Frame = {binary, ExitBin}, + {[Frame, close], State}. -terminate(_Reason, _Req, #{server := Server}) -> - ?LOG_ALERT("CLI: terminate: ~p", [_Reason]), - rabbit_cli_http_server:stop(Server), - receive - {'EXIT', Server, _} -> - ok - end, - ok; -terminate(_Reason, _Req, _State) -> - ?LOG_ALERT("CLI: terminate: ~p", [_Reason]), +terminate(Reason, _Req, _State) -> + ?LOG_DEBUG("CLI: HTTP server terminating: ~0p", [Reason]), ok. reply_with_help(Req, Code) -> @@ -197,3 +179,21 @@ reply_with_help(Req, Code) -> cowboy_req:reply( Code, #{<<"content-type">> => <<"text/html; charset=utf-8">>}, Body, Req). + +handle_request({call, From, Command}) -> + Ret = handle_command(Command), + Reply = {call_ret, From, Ret}, + {reply, Reply}; +handle_request({cast, Command}) -> + _ = handle_command(Command), + noreply. + +handle_command({run_command, ContextMap}) -> + Caller = self(), + rabbit_cli_backend:run_command(ContextMap, Caller); +handle_command({rpc, Module, Function, Args}) -> + erlang:apply(Module, Function, Args); +handle_command({link, Pid}) -> + erlang:link(Pid); +handle_command({send, Dest, Msg}) -> + erlang:send(Dest, Msg). diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index 3eea66f3bcb4..50f731009d87 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -3,7 +3,9 @@ -include_lib("kernel/include/logger.hrl"). -export([connect/0, connect/1, + run_command/2, rpc/4, + link/2, send/3]). -record(?MODULE, {type :: erldist | http, @@ -85,12 +87,23 @@ complete_nodename(Nodename) -> list_to_atom(Nodename) end. +run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> + Caller = self(), + erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); +run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> + rabbit_cli_http_client:run_command(Client, ContextMap). + rpc(#?MODULE{type = erldist, peer = Node}, Module, Function, Args) -> erpc:call(Node, Module, Function, Args); -rpc(#?MODULE{type = http, peer = Pid}, Module, Function, Args) -> - rabbit_cli_http_client:rpc(Pid, Module, Function, Args). +rpc(#?MODULE{type = http, peer = Client}, Module, Function, Args) -> + rabbit_cli_http_client:rpc(Client, Module, Function, Args). + +link(#?MODULE{type = erldist}, Pid) -> + erlang:link(Pid); +link(#?MODULE{type = http, peer = Client}, Pid) -> + rabbit_cli_http_client:link(Client, Pid). send(#?MODULE{type = erldist}, Dest, Msg) -> erlang:send(Dest, Msg); -send(#?MODULE{type = http, peer = Pid}, Dest, Msg) -> - rabbit_cli_http_client:send(Pid, Dest, Msg). +send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> + rabbit_cli_http_client:send(Client, Dest, Msg). From 854a9c396575fccd22e57410f0179a9869c01799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 20 Jun 2025 17:27:11 +0200 Subject: [PATCH 27/51] Fix log messages loss when escript exit --- deps/rabbit/src/rabbit_cli_frontend.erl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index babd9a7c889d..4d88b118d277 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -18,8 +18,8 @@ main(Args) -> Ret = run_cli(ScriptName, Args), ?LOG_NOTICE("CLI: run_cli() return value: ~p", [Ret]), - %% FIXME: Ensures everything written to stdout/stderr was flushed. - timer:sleep(50), + + flush_log_messages(), erlang:halt(). %% ------------------------------------------------------------------- @@ -37,6 +37,8 @@ add_rabbitmq_code_path(ScriptName) -> AppDirs = filelib:wildcard(Glob), lists:foreach(fun code:add_path/1, AppDirs). +-define(LOG_HANDLER_NAME, rmq_cli). + configure_logging() -> Config = #{level => debug, config => #{type => standard_error}, @@ -45,10 +47,13 @@ configure_logging() -> formatter => {rabbit_logger_text_fmt, #{single_line => false, use_colors => true}}}, - ok = logger:add_handler(rmq_cli, rabbit_logger_std_h, Config), + ok = logger:add_handler(?LOG_HANDLER_NAME, rabbit_logger_std_h, Config), ok = logger:remove_handler(default), ok. +flush_log_messages() -> + rabbit_logger_std_h:filesync(?LOG_HANDLER_NAME). + %% ------------------------------------------------------------------- %% Preparation for remote command execution. %% ------------------------------------------------------------------- @@ -199,7 +204,7 @@ run_command(#rabbit_cli{} = Context) -> maybe process_flag(trap_exit, true), ContextMap = context_to_map(Context), - {ok, _Backend} ?= rabbit_cli_backend:run_command(ContextMap), + {ok, _Backend} ?= rabbit_cli_backend:run_command(ContextMap, self()), main_loop(Context) end. From 063929d5fced71e9e3b9d6472f8d2cedf2e74e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 20 Jun 2025 17:27:48 +0200 Subject: [PATCH 28/51] Remove unused functions --- deps/rabbit/src/rabbit_cli_backend.erl | 4 ++ deps/rabbit/src/rabbit_cli_http_client.erl | 67 ++++++-------------- deps/rabbit/src/rabbit_cli_http_listener.erl | 12 ++-- deps/rabbit/src/rabbit_cli_transport2.erl | 20 +----- 4 files changed, 29 insertions(+), 74 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 4ffc76583f37..d24700e001f1 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -44,6 +44,10 @@ start_link(Context, Caller, GroupLeader) -> group_leader => GroupLeader}, gen_statem:start_link(?MODULE, Args, []). +%% ------------------------------------------------------------------- +%% gen_statem callbacks. +%% ------------------------------------------------------------------- + init(#{context := Context, caller := Caller, group_leader := GroupLeader}) -> process_flag(trap_exit, true), erlang:link(Caller), diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index 5c10e22ee589..8590555fafc3 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -4,11 +4,8 @@ -include_lib("kernel/include/logger.hrl"). --export([start_link/1, t/0, - run_command/2, - rpc/4, - link/2, - send/3]). +-export([start_link/1, + run_command/2]). -export([init/1, callback_mode/0, handle_event/4, @@ -22,25 +19,15 @@ io_requests = #{} :: map(), group_leader :: pid()}). -t() -> - {ok, P} = start_link("http://localhost:8080"), - Data = rpc(P, io, get_line, ["Prompt: "]), - rpc(P, io, format, ["Data: ~p~n", [Data]]). - start_link(Uri) -> gen_statem:start_link(?MODULE, Uri, []). run_command(Client, ContextMap) -> gen_statem:call(Client, {?FUNCTION_NAME, ContextMap}). -rpc(Client, Module, Function, Args) -> - gen_statem:call(Client, {?FUNCTION_NAME, Module, Function, Args}). - -link(Client, Pid) -> - gen_statem:call(Client, {?FUNCTION_NAME, Pid}). - -send(Client, Dest, Msg) -> - gen_statem:cast(Client, {?FUNCTION_NAME, Dest, Msg}). +%% ------------------------------------------------------------------- +%% gen_statem callbacks. +%% ------------------------------------------------------------------- init(Uri) -> maybe @@ -63,53 +50,36 @@ callback_mode() -> handle_event( info, {gun_up, ConnPid, _}, - opening_connection, - #?MODULE{connection = ConnPid} = Data) -> + opening_connection, #?MODULE{connection = ConnPid} = Data) -> ?LOG_DEBUG("CLI: HTTP connection opened, upgrading to websocket"), StreamRef = gun:ws_upgrade(ConnPid, "/", []), Data1 = Data#?MODULE{stream = StreamRef}, {next_state, opening_stream, Data1}; handle_event( info, {gun_upgrade, _ConnPid, _StreamRef, _Frames, _}, - opening_stream, - #?MODULE{} = Data) -> + opening_stream, #?MODULE{} = Data) -> ?LOG_DEBUG("CLI: websocket ready, sending pending requests"), Data1 = flush_delayed_requests(Data), {next_state, stream_ready, Data1}; -%% Call (e.g. RPC). -handle_event( - {call, From}, Command, - stream_ready, - #?MODULE{} = Data) -> +handle_event({call, From}, Command, stream_ready, #?MODULE{} = Data) -> Request = prepare_call(From, Command), send_request(Request, Data), {keep_state, Data}; -handle_event( - {call, From}, Command, - _State, - #?MODULE{} = Data) -> +handle_event({call, From}, Command, _State, #?MODULE{} = Data) -> Request = prepare_call(From, Command), Data1 = delay_request(Request, Data), {keep_state, Data1}; -%% Cast (e.g. send). -handle_event( - cast, Command, - stream_ready, - #?MODULE{} = Data) -> +handle_event(cast, Command, stream_ready, #?MODULE{} = Data) -> Request = prepare_cast(Command), send_request(Request, Data), {keep_state, Data}; -handle_event( - cast, Command, - _State, - #?MODULE{} = Data) -> +handle_event(cast, Command, _State, #?MODULE{} = Data) -> Request = prepare_cast(Command), Data1 = delay_request(Request, Data), {keep_state, Data1}; handle_event( info, {gun_ws, _ConnPid, _StreamRef, {binary, RequestBin}}, - stream_ready, - #?MODULE{} = Data) -> + stream_ready, #?MODULE{} = Data) -> Request = binary_to_term(RequestBin), ?LOG_DEBUG("CLI: received HTTP message from server: ~p", [Request]), case handle_request(Request, Data) of @@ -123,8 +93,7 @@ handle_event( end; handle_event( info, {io_reply, ProxyRef, Reply}, - _State, - #?MODULE{io_requests = IoRequests} = Data) -> + _State, #?MODULE{io_requests = IoRequests} = Data) -> {From, ReplyAs} = maps:get(ProxyRef, IoRequests), IoReply = {io_reply, ReplyAs, Reply}, Command = {send, From, IoReply}, @@ -135,15 +104,13 @@ handle_event( {keep_state, Data1}; handle_event( info, {gun_ws, _ConnPid, _StreamRef, {close, _, _}}, - stream_ready, - #?MODULE{} = Data) -> + stream_ready, #?MODULE{} = Data) -> ?LOG_DEBUG("CLI: stream closed"), %% FIXME: Handle pending requests. {stop, normal, Data}; handle_event( info, {gun_down, _ConnPid, _Proto, _Reason, _KilledStreams}, - _State, - #?MODULE{} = Data) -> + _State, #?MODULE{} = Data) -> ?LOG_DEBUG("CLI: gun_down: ~p", [_Reason]), %% FIXME: Handle pending requests. {stop, normal, Data}. @@ -155,6 +122,10 @@ terminate(Reason, _State, _Data) -> code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. +%% ------------------------------------------------------------------- +%% Internal functions. +%% ------------------------------------------------------------------- + prepare_call(From, Command) -> {call, From, Command}. diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl index 96818c2b399f..4e99a6ec5559 100644 --- a/deps/rabbit/src/rabbit_cli_http_listener.erl +++ b/deps/rabbit/src/rabbit_cli_http_listener.erl @@ -167,6 +167,10 @@ terminate(Reason, _Req, _State) -> ?LOG_DEBUG("CLI: HTTP server terminating: ~0p", [Reason]), ok. +%% ------------------------------------------------------------------- +%% Internal functions. +%% ------------------------------------------------------------------- + reply_with_help(Req, Code) -> PrivDir = code:priv_dir(rabbit), HelpFilename = filename:join(PrivDir, "cli_http_help.html"), @@ -190,10 +194,4 @@ handle_request({cast, Command}) -> handle_command({run_command, ContextMap}) -> Caller = self(), - rabbit_cli_backend:run_command(ContextMap, Caller); -handle_command({rpc, Module, Function, Args}) -> - erlang:apply(Module, Function, Args); -handle_command({link, Pid}) -> - erlang:link(Pid); -handle_command({send, Dest, Msg}) -> - erlang:send(Dest, Msg). + rabbit_cli_backend:run_command(ContextMap, Caller). diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index 50f731009d87..d2d9ce2c3af4 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -3,10 +3,7 @@ -include_lib("kernel/include/logger.hrl"). -export([connect/0, connect/1, - run_command/2, - rpc/4, - link/2, - send/3]). + run_command/2]). -record(?MODULE, {type :: erldist | http, peer :: atom() | pid()}). @@ -92,18 +89,3 @@ run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> rabbit_cli_http_client:run_command(Client, ContextMap). - -rpc(#?MODULE{type = erldist, peer = Node}, Module, Function, Args) -> - erpc:call(Node, Module, Function, Args); -rpc(#?MODULE{type = http, peer = Client}, Module, Function, Args) -> - rabbit_cli_http_client:rpc(Client, Module, Function, Args). - -link(#?MODULE{type = erldist}, Pid) -> - erlang:link(Pid); -link(#?MODULE{type = http, peer = Client}, Pid) -> - rabbit_cli_http_client:link(Client, Pid). - -send(#?MODULE{type = erldist}, Dest, Msg) -> - erlang:send(Dest, Msg); -send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> - rabbit_cli_http_client:send(Client, Dest, Msg). From b31b621b0b342eb2aa7093b93186a442437db7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 23 Jun 2025 13:06:13 +0200 Subject: [PATCH 29/51] Re-implement "list exchanges" with datagrid --- deps/rabbit/src/rabbit_cli_backend.erl | 10 +- deps/rabbit/src/rabbit_cli_backend.hrl | 2 +- deps/rabbit/src/rabbit_cli_commands.erl | 82 +++++----------- deps/rabbit/src/rabbit_cli_datagrid.erl | 125 ++++++++++++++++++++++++ deps/rabbit/src/rabbit_cli_frontend.erl | 16 +-- deps/rabbit/src/rabbit_exchange.erl | 109 +++++++++++++++++++++ 6 files changed, 275 insertions(+), 69 deletions(-) create mode 100644 deps/rabbit/src/rabbit_cli_datagrid.erl diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index d24700e001f1..ae37d234cb2a 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -17,6 +17,8 @@ terminate/3, code_change/4]). +-record(?MODULE, {caller}). + %% TODO: %% * Implémenter "list exchanges" plus proprement %% * Implémenter "rabbitmqctl list_exchanges" pour la compatibilité @@ -34,9 +36,7 @@ map_to_context(ContextMap) -> argparse_def = maps:get(argparse_def, ContextMap), arg_map = maps:get(arg_map, ContextMap), cmd_path = maps:get(cmd_path, ContextMap), - command = maps:get(command, ContextMap), - - frontend_priv = undefined}. + command = maps:get(command, ContextMap)}. start_link(Context, Caller, GroupLeader) -> Args = #{context => Context, @@ -52,7 +52,9 @@ init(#{context := Context, caller := Caller, group_leader := GroupLeader}) -> process_flag(trap_exit, true), erlang:link(Caller), erlang:group_leader(GroupLeader, self()), - {ok, standing_by, Context, {next_event, internal, parse_command}}. + Priv = #?MODULE{caller = Caller}, + Context1 = Context#rabbit_cli{priv = Priv}, + {ok, standing_by, Context1, {next_event, internal, parse_command}}. callback_mode() -> handle_event_function. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index e0df9c7077e2..c52d5d8de3bb 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -5,4 +5,4 @@ cmd_path, command, - frontend_priv}). + priv}). diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index a0967dcda901..5e43e1e0f6f0 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -5,13 +5,15 @@ -include_lib("rabbit_common/include/logging.hrl"). -include_lib("rabbit_common/include/resource.hrl"). +-include("src/rabbit_cli_backend.hrl"). + -export([discover_commands/0, discovered_commands/0, - discovered_argparse_def/0]). + discovered_argparse_def/0, + expect_legacy/1]). -export([cmd_noop/1, cmd_hello/1, cmd_crash/1, - cmd_list_exchanges/1, cmd_import_definitions/1, cmd_top/1]). @@ -46,13 +48,6 @@ ], handler => {?MODULE, cmd_declare_exchange}}}). --rabbitmq_command( - {#{cli => ["list", "exchanges"], - http => {get, ["exchanges"]}}, - [argparse_def_record_stream, - #{help => "List exchanges", - handler => {?MODULE, cmd_list_exchanges}}]}). - -rabbitmq_command( {#{cli => ["import", "definitions"]}, [argparse_def_file_input, @@ -97,7 +92,6 @@ discovered_argparse_def() -> do_discover_commands() -> %% Extract the commands from module attributes like feature flags and boot %% steps. - %% TODO: Discover commands as a boot step. %% TODO: Write shell completion scripts for various shells as part of that. %% TODO: Generate manpages? When/how? With eDoc? ?LOG_DEBUG( @@ -144,11 +138,33 @@ expand_argparse_def(Defs) when is_list(Defs) -> (argparse_def_file_input, Acc) -> Def = rabbit_cli_io:argparse_def(file_input), rabbit_cli:merge_argparse_def(Acc, Def); + (Mod, Acc) when is_atom(Mod) -> + Def = Mod:argparse_def(), + rabbit_cli:merge_argparse_def(Acc, Def); (Def, Acc) -> Def1 = expand_argparse_def(Def), rabbit_cli:merge_argparse_def(Acc, Def1) end, #{}, Defs). +%% ------------------------------------------------------------------- +%% Helpers. +%% ------------------------------------------------------------------- + +expect_legacy(#rabbit_cli{progname = <<"rabbitmqctl">>}) -> + true; +expect_legacy(#rabbit_cli{progname = <<"rabbitmq-diagnostics">>}) -> + true; +expect_legacy(#rabbit_cli{progname = <<"rabbitmq-plugins">>}) -> + true; +expect_legacy(#rabbit_cli{progname = <<"rabbitmq-queues">>}) -> + true; +expect_legacy(#rabbit_cli{progname = <<"rabbitmq-streams">>}) -> + true; +expect_legacy(#rabbit_cli{progname = <<"rabbitmq-upgrade">>}) -> + true; +expect_legacy(_Context) -> + false. + %% ------------------------------------------------------------------- %% XXX %% ------------------------------------------------------------------- @@ -164,52 +180,6 @@ cmd_hello(_) -> cmd_crash(_) -> erlang:exit(oops). -cmd_list_exchanges(#{progname := Progname, arg_map := ArgMap}) -> - logger:alert("CLI: running list exchanges"), - InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], - Fields = lists:map( - fun - (name = Key) -> - #{name => Key, type => string}; - (type = Key) -> - #{name => Key, type => string}; - (durable = Key) -> - #{name => Key, type => boolean}; - (auto_delete = Key) -> - #{name => Key, type => boolean}; - (internal = Key) -> - #{name => Key, type => boolean}; - (arguments = Key) -> - #{name => Key, type => term}; - (policy = Key) -> - #{name => Key, type => string}; - (Key) -> - #{name => Key, type => term} - end, InfoKeys), - {ok, IO} = rabbit_cli_io:start_link(Progname), - Ret = case rabbit_cli_io:start_record_stream(IO, exchanges, Fields, ArgMap) of - {ok, Stream} -> - Exchanges = rabbit_exchange:list(), - lists:foreach( - fun(Exchange) -> - Record0 = rabbit_exchange:info(Exchange, InfoKeys), - Record1 = lists:sublist(Record0, length(Fields)), - Record2 = [case Value of - #resource{name = N} -> - N; - _ -> - Value - end || {_Key, Value} <- Record1], - rabbit_cli_io:push_new_record(IO, Stream, Record2) - end, Exchanges), - rabbit_cli_io:end_record_stream(IO, Stream), - ok; - {error, _} = Error -> - Error - end, - rabbit_cli_io:stop(IO), - Ret. - cmd_import_definitions(#{progname := Progname, arg_map := ArgMap}) -> {ok, IO} = rabbit_cli_io:start_link(Progname), %% TODO: Use a wrapper above `file' to proxy through transport. diff --git a/deps/rabbit/src/rabbit_cli_datagrid.erl b/deps/rabbit/src/rabbit_cli_datagrid.erl new file mode 100644 index 000000000000..acd6c81c5f6b --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_datagrid.erl @@ -0,0 +1,125 @@ +-module(rabbit_cli_datagrid). + +-include_lib("kernel/include/logger.hrl"). + +-include("src/rabbit_cli_backend.hrl"). + +-export([argparse_def/0, + process/6]). + +-record(?MODULE, {fields_fun, + setup_stream_fun, + next_record_fun, + teardown_stream_fun, + + fields, + priv, + context}). + +argparse_def() -> + #{arguments => + [ + #{name => output, + long => "-output", + short => $o, + type => string, + nargs => 1, + help => "Write output to file "}, + #{name => format, + long => "-format", + short => $f, + type => {atom, [plain, legacy_plain, json]}, + default => plain, + help => "Format output acccording to "}, + #{name => fields, + type => binary, + nargs => list, + required => false, + help => "Fields to include"} + ] + }. + +process( + FieldsFun, SetupStreamFun, NextRecordFun, TeardownStreamFun, + Priv, Context) -> + State = #?MODULE{fields_fun = FieldsFun, + setup_stream_fun = SetupStreamFun, + next_record_fun = NextRecordFun, + teardown_stream_fun = TeardownStreamFun, + + priv = Priv, + context = Context}, + process_fields(State). + +process_fields(#?MODULE{fields_fun = FieldsFun, priv = Priv} = State) -> + maybe + {ok, Fields, Priv1} ?= FieldsFun(Priv), + State1 = State#?MODULE{fields = Fields, + priv = Priv1}, + + {ok, State2} ?= format_fields(State1), + start_stream(State2) + end. + +start_stream( + #?MODULE{setup_stream_fun = SetupStreamFun, priv = Priv} = State) -> + maybe + {ok, Priv1} ?= SetupStreamFun(Priv), + State1 = State#?MODULE{priv = Priv1}, + process_records(State1) + end. + +process_records( + #?MODULE{next_record_fun = NextRecordFun, priv = Priv} = State) -> + case NextRecordFun(Priv) of + {ok, Record, Priv1} when is_list(Record) -> + maybe + State1 = State#?MODULE{priv = Priv1}, + {ok, State2} ?= format_record(Record, State1), + process_records(State2) + end; + {ok, none, Priv1} -> + State1 = State#?MODULE{priv = Priv1}, + stop_stream(State1) + end. + +stop_stream(#?MODULE{teardown_stream_fun = TeardownStreamFun, priv = Priv}) -> + TeardownStreamFun(Priv). + +format_fields(#?MODULE{fields = Fields} = State) -> + Fields1 = [rabbit_misc:format("~s", [Name]) || #{name := Name} <- Fields], + Fields2 = string:join(Fields1, "\t"), + io:format("~ts~n", [Fields2]), + {ok, State}. + +format_record(Record, #?MODULE{fields = Fields} = State) -> + Values1 = format_values(Fields, Record), + Values2 = string:join(Values1, "\t"), + io:format("~ts~n", [Values2]), + {ok, State}. + +format_values(Fields, Values) -> + format_values(Fields, Values, []). + +format_values([#{type := string} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~ts", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, Acc1); +format_values([#{type := binary} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~-20.. ts", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, Acc1); +format_values([#{type := integer} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~b", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, Acc1); +format_values([#{type := boolean} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~ts", [if Value -> "☑"; true -> "☐" end]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, Acc1); +format_values([#{type := term} | Rest1], [Value | Rest2], Acc) -> + String = io_lib:format("~0p", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, Acc1); +format_values([], [], Acc) -> + lists:reverse(Acc). diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 4d88b118d277..6c254f20b307 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -17,7 +17,7 @@ main(Args) -> configure_logging(), Ret = run_cli(ScriptName, Args), - ?LOG_NOTICE("CLI: run_cli() return value: ~p", [Ret]), + ?LOG_DEBUG("CLI: run_cli() return value: ~p", [Ret]), flush_log_messages(), erlang:halt(). @@ -61,9 +61,9 @@ flush_log_messages() -> run_cli(ScriptName, Args) -> ProgName = filename:basename(ScriptName, ".escript"), Priv = #?MODULE{scriptname = ScriptName}, - Context = #rabbit_cli{progname = ProgName, + Context = #rabbit_cli{progname = list_to_binary(ProgName), args = Args, - frontend_priv = Priv}, + priv = Priv}, init_local_args(Context). init_local_args(Context) -> @@ -89,7 +89,7 @@ set_log_level(#rabbit_cli{} = Context) -> connect_to_node(Context). connect_to_node( - #rabbit_cli{arg_map = ArgMap, frontend_priv = Priv} = Context) -> + #rabbit_cli{arg_map = ArgMap, priv = Priv} = Context) -> Ret = case ArgMap of #{node := NodenameOrUri} -> rabbit_cli_transport2:connect(NodenameOrUri); @@ -102,7 +102,7 @@ connect_to_node( {error, _Reason} -> Priv#?MODULE{connection = none} end, - Context1 = Context#rabbit_cli{frontend_priv = Priv1}, + Context1 = Context#rabbit_cli{priv = Priv1}, run_command(Context1). %% ------------------------------------------------------------------- @@ -186,7 +186,7 @@ noop(_Context) -> %% * evolutions in the communication between the frontend and the backend run_command( - #rabbit_cli{frontend_priv = #?MODULE{connection = Connection}} = Context) + #rabbit_cli{priv = #?MODULE{connection = Connection}} = Context) when Connection =/= none -> maybe process_flag(trap_exit, true), @@ -211,8 +211,8 @@ run_command(#rabbit_cli{} = Context) -> context_to_map(Context) -> Fields = [Field || Field <- record_info(fields, rabbit_cli), %% We don't need or want to communicate anything that - %% is private to the frontend. - Field =/= frontend_priv], + %% is private to the backend. + Field =/= priv], record_to_map(Fields, Context, 2, #{}). record_to_map([Field | Rest], Record, Index, Map) -> diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index 8a57123ad67f..61b83b7be0aa 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -8,6 +8,8 @@ -module(rabbit_exchange). -include_lib("rabbit_common/include/rabbit.hrl"). +-include("src/rabbit_cli_backend.hrl"). + -export([recover/1, policy_changed/2, callback/4, declare/7, assert_equivalence/6, assert_args_equivalence/2, check_type/1, exists/1, lookup/1, lookup_many/1, lookup_or_die/1, list/0, list/1, lookup_scratch/2, @@ -19,6 +21,16 @@ -export([serialise_events/1]). -export([serial/1, peek_serial/1]). +%% CLI commands. +-export([cmd_list_exchanges/1]). + +-rabbitmq_command( + {#{cli => ["list", "exchanges"], + http => {get, ["exchanges"]}}, + [rabbit_cli_datagrid, + #{help => "List exchanges", + handler => {?MODULE, cmd_list_exchanges}}]}). + %%---------------------------------------------------------------------------- -deprecated([{route, 2, "Use route/3 instead"}]). @@ -561,3 +573,100 @@ type_to_route_fun(T) -> FunArity -> FunArity end. + +%% ------------------------------------------------------------------- +%% CLI commands. +%% ------------------------------------------------------------------- + +cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap} = Context) -> + InfoKeys0 = rabbit_exchange:info_keys() -- [user_who_performed_action], + InfoKeys1 = [atom_to_binary(I) || I <- InfoKeys0], + Fields0 = case ArgMap of + #{fields := Arg} -> + Arg; + _ -> + [name, type] + end, + Fields1 = lists:filtermap( + fun(Field) -> + IsValid = lists:member(Field, InfoKeys1), + case IsValid of + true -> + {true, binary_to_atom(Field)}; + false -> + false + end + end, Fields0), + Priv = #{fields => Fields1}, + + %% Start datagrid with callbacks. + rabbit_cli_datagrid:process( + fun exchanges_fields/1, + fun exchanges_setup_stream/1, + fun exchanges_next_record/1, + fun exchanges_teardown_stream/1, + Priv, Context). + +exchanges_fields( + #{fields := Fields} = Priv) -> + Fields1 = lists:map( + fun + (name = Key) -> + #{name => Key, type => string}; + (type = Key) -> + #{name => Key, type => string}; + (durable = Key) -> + #{name => Key, type => boolean}; + (auto_delete = Key) -> + #{name => Key, type => boolean}; + (internal = Key) -> + #{name => Key, type => boolean}; + (arguments = Key) -> + #{name => Key, type => term}; + (policy = Key) -> + #{name => Key, type => string}; + (Key) -> + #{name => Key, type => term} + end, Fields), + {ok, Fields1, Priv}. + +exchanges_setup_stream(Priv) -> + Exchanges = rabbit_exchange:list(), + case Exchanges of + [First | _] -> + Next = First#exchange.name, + {ok, Priv#{exchanges => Exchanges, next => Next}}; + [] -> + {ok, Priv#{exchanges => Exchanges}} + end. + +exchanges_teardown_stream(_Priv) -> + ok. + +exchanges_next_record(#{exchanges := Exchanges, next := Name} = Priv) -> + exchanges_next_record1(Exchanges, Name, Priv); +exchanges_next_record(Priv) -> + {ok, none, Priv}. + +exchanges_next_record1( + [#exchange{name = Name1} = Exchange | Rest], Name2, + #{fields := Fields} = Priv) + when Name1 =:= Name2 -> + Record0 = info(Exchange, Fields), + Record1 = lists:sublist(Record0, length(Fields)), + Record2 = [case Value of + #resource{name = N} -> + N; + _ -> + Value + end || {_Key, Value} <- Record1], + + Priv1 = case Rest of + [#exchange{name = Next} | _] -> + Priv#{next => Next}; + [] -> + maps:remove(next, Priv) + end, + {ok, Record2, Priv1}; +exchanges_next_record1([_ | Rest], Name, Priv) -> + exchanges_next_record1(Rest, Name, Priv). From 2adb5b17e2e7c90efcaedbf20878bbe996efb8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 23 Jun 2025 13:14:25 +0200 Subject: [PATCH 30/51] Re-implement --help --- deps/rabbit/src/rabbit_cli_backend.erl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index ae37d234cb2a..7d3fc55e4ba2 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -75,6 +75,11 @@ handle_event(internal, parse_command, standing_by, Context) -> {error, Reason} -> {stop, {failed_to_parse_command, Reason}} end; +handle_event( + internal, run_command, command_parsed, + #rabbit_cli{arg_map = #{help := true}} = Context) -> + display_help(Context), + {stop, {shutdown, ok}, Context}; handle_event(internal, run_command, command_parsed, Context) -> Ret = do_run_command(Context), {stop, {shutdown, Ret}, Context}. @@ -125,3 +130,15 @@ final_parse( do_run_command( #rabbit_cli{command = #{handler := {Module, Function}}} = Context) -> erlang:apply(Module, Function, [Context]). + +display_help(#rabbit_cli{progname = Progname, + argparse_def = ArgparseDef, + arg_map = #{help := true}, + cmd_path = CmdPath}) -> + Options = #{progname => Progname, + %% Work around bug in argparse; + %% See https://github.com/erlang/otp/pull/9160 + command => tl(CmdPath)}, + Help = argparse:help(ArgparseDef, Options), + io:format("~s~n", [Help]), + ok. From 4b7e3a2af87c09a695b2e15e09a55f2f4b93f432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 23 Jun 2025 19:24:53 +0200 Subject: [PATCH 31/51] Move argparse_def merge function to `rabbit_cli_commands` --- deps/rabbit/src/rabbit_cli_backend.erl | 20 +--------- deps/rabbit/src/rabbit_cli_commands.erl | 51 ++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 7d3fc55e4ba2..636692a3ffcb 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -98,26 +98,10 @@ code_change(_Vsn, State, Data, _Extra) -> final_argparse_def( #rabbit_cli{argparse_def = PartialArgparseDef}) -> FullArgparseDef = rabbit_cli_commands:discovered_argparse_def(), - ArgparseDef1 = merge_argparse_def(PartialArgparseDef, FullArgparseDef), + ArgparseDef1 = rabbit_cli_commands:merge_argparse_def( + PartialArgparseDef, FullArgparseDef), ArgparseDef1. -merge_argparse_def(ArgparseDef1, ArgparseDef2) -> - Args1 = maps:get(arguments, ArgparseDef1, []), - Args2 = maps:get(arguments, ArgparseDef2, []), - Args = merge_arguments(Args1, Args2), - Cmds1 = maps:get(commands, ArgparseDef1, #{}), - Cmds2 = maps:get(commands, ArgparseDef2, #{}), - Cmds = merge_commands(Cmds1, Cmds2), - maps:merge( - ArgparseDef1, - ArgparseDef2#{arguments => Args, commands => Cmds}). - -merge_arguments(Args1, Args2) -> - Args1 ++ Args2. - -merge_commands(Cmds1, Cmds2) -> - maps:merge(Cmds1, Cmds2). - final_parse( #rabbit_cli{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> Options = #{progname => ProgName}, diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 5e43e1e0f6f0..e2b95177e818 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -10,6 +10,7 @@ -export([discover_commands/0, discovered_commands/0, discovered_argparse_def/0, + merge_argparse_def/2, expect_legacy/1]). -export([cmd_noop/1, cmd_hello/1, @@ -121,12 +122,16 @@ commands_to_cli_argparse_def(Commands) -> (Cmd, M0) -> #{commands => #{Cmd => M0}} end, undefined, Path), - rabbit_cli:merge_argparse_def(Acc1, M1); + merge_argparse_def(Acc1, M1); (_, Acc1) -> Acc1 end, Acc0, Entries) end, #{}, Commands). +%% ------------------------------------------------------------------- +%% Argparse helpers. +%% ------------------------------------------------------------------- + expand_argparse_def(Def) when is_map(Def) -> Def; expand_argparse_def(Defs) when is_list(Defs) -> @@ -134,18 +139,54 @@ expand_argparse_def(Defs) when is_list(Defs) -> fun (argparse_def_record_stream, Acc) -> Def = rabbit_cli_io:argparse_def(record_stream), - rabbit_cli:merge_argparse_def(Acc, Def); + merge_argparse_def(Acc, Def); (argparse_def_file_input, Acc) -> Def = rabbit_cli_io:argparse_def(file_input), - rabbit_cli:merge_argparse_def(Acc, Def); + merge_argparse_def(Acc, Def); (Mod, Acc) when is_atom(Mod) -> Def = Mod:argparse_def(), - rabbit_cli:merge_argparse_def(Acc, Def); + merge_argparse_def(Acc, Def); (Def, Acc) -> Def1 = expand_argparse_def(Def), - rabbit_cli:merge_argparse_def(Acc, Def1) + merge_argparse_def(Acc, Def1) end, #{}, Defs). +merge_argparse_def(ArgparseDef1, ArgparseDef2) -> + Args1 = maps:get(arguments, ArgparseDef1, []), + Args2 = maps:get(arguments, ArgparseDef2, []), + Args = merge_arguments(Args1, Args2), + Cmds1 = maps:get(commands, ArgparseDef1, #{}), + Cmds2 = maps:get(commands, ArgparseDef2, #{}), + Cmds = merge_commands(Cmds1, Cmds2), + maps:merge( + ArgparseDef1, + ArgparseDef2#{arguments => Args, commands => Cmds}). + +merge_arguments(Args1, []) -> + Args1; +merge_arguments([], Args2) -> + Args2; +merge_arguments(Args1, Args2) -> + merge_arguments(Args1, Args2, []). + +merge_arguments([#{name := Name} = Arg1 | Rest1], Args2, Acc) -> + Ret = lists:partition( + fun(#{name := N}) -> N =:= Name end, + Args2), + {Arg, Rest2} = case Ret of + {[Arg2], NotMatching} -> + {Arg2, NotMatching}; + {[], Args2} -> + {Arg1, Args2} + end, + Acc1 = [Arg | Acc], + merge_arguments(Rest1, Rest2, Acc1); +merge_arguments([], Args2, Acc) -> + lists:reverse(Acc) ++ Args2. + +merge_commands(Cmds1, Cmds2) -> + maps:merge(Cmds1, Cmds2). + %% ------------------------------------------------------------------- %% Helpers. %% ------------------------------------------------------------------- From 4d1090c7f136220bb2230a8bdcb9c9ddadbce901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 23 Jun 2025 19:25:51 +0200 Subject: [PATCH 32/51] Add columns choices to `list exchanges` --- deps/rabbit/src/rabbit_exchange.erl | 36 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index 61b83b7be0aa..71d1cba8a9ac 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -24,13 +24,6 @@ %% CLI commands. -export([cmd_list_exchanges/1]). --rabbitmq_command( - {#{cli => ["list", "exchanges"], - http => {get, ["exchanges"]}}, - [rabbit_cli_datagrid, - #{help => "List exchanges", - handler => {?MODULE, cmd_list_exchanges}}]}). - %%---------------------------------------------------------------------------- -deprecated([{route, 2, "Use route/3 instead"}]). @@ -50,6 +43,22 @@ policy, user_who_performed_action]). -define(DEFAULT_EXCHANGE_NAME, <<>>). +-rabbitmq_command( + {#{cli => ["list", "exchanges"], + http => {get, ["exchanges"]}}, + [rabbit_cli_datagrid, + #{help => "List exchanges", + arguments => + [ + #{name => fields, + %% FIXME: Exclude user_who_performed_action + type => {atom, ?INFO_KEYS}, + nargs => list, + required => false, + help => "Fields to include"} + ], + handler => {?MODULE, cmd_list_exchanges}}]}). + -spec recover(rabbit_types:vhost()) -> [name()]. recover(VHost) -> @@ -579,23 +588,16 @@ type_to_route_fun(T) -> %% ------------------------------------------------------------------- cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap} = Context) -> - InfoKeys0 = rabbit_exchange:info_keys() -- [user_who_performed_action], - InfoKeys1 = [atom_to_binary(I) || I <- InfoKeys0], + InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], Fields0 = case ArgMap of #{fields := Arg} -> Arg; _ -> [name, type] end, - Fields1 = lists:filtermap( + Fields1 = lists:filter( fun(Field) -> - IsValid = lists:member(Field, InfoKeys1), - case IsValid of - true -> - {true, binary_to_atom(Field)}; - false -> - false - end + lists:member(Field, InfoKeys) end, Fields0), Priv = #{fields => Fields1}, From 26aa0472226d596bc87b1b3633c981c8114b1b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 23 Jun 2025 19:26:25 +0200 Subject: [PATCH 33/51] Generate fish shell completion script --- deps/rabbit/src/rabbit_cli_commands.erl | 174 +++++++++++++++++++++++- 1 file changed, 173 insertions(+), 1 deletion(-) diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index e2b95177e818..512c817f866e 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -12,7 +12,8 @@ discovered_argparse_def/0, merge_argparse_def/2, expect_legacy/1]). --export([cmd_noop/1, +-export([cmd_generate_completion_script/1, + cmd_noop/1, cmd_hello/1, cmd_crash/1, cmd_import_definitions/1, @@ -60,6 +61,17 @@ [#{help => "Top-like interactive view", handler => {?MODULE, cmd_top}}]}). +-rabbitmq_command( + {#{cli => ["generate", "completion"]}, + #{help => "Generate a completion script for the given shell", + arguments => [ + #{name => shell, + type => {binary, [<<"fish">>]}, + required => true, + help => "Name of the shell to target"} + ], + handler => {?MODULE, cmd_generate_completion_script}}}). + %% ------------------------------------------------------------------- %% Commands discovery. %% ------------------------------------------------------------------- @@ -187,6 +199,166 @@ merge_arguments([], Args2, Acc) -> merge_commands(Cmds1, Cmds2) -> maps:merge(Cmds1, Cmds2). +%% ------------------------------------------------------------------- +%% Completion files. +%% ------------------------------------------------------------------- + +cmd_generate_completion_script( + #rabbit_cli{arg_map = #{shell := Shell}} = Context) -> + ?LOG_DEBUG("Generating completion script for shell `~ts`", [Shell]), + generate_completion_script(Context, Shell). + +generate_completion_script( + #rabbit_cli{progname = Progname, argparse_def = ArgparseDef} = Context, + <<"fish">>) -> + Chunk1 = io_lib:format( + """ + # Clear any existing completion rules. + complete -c ~ts -e + + # Disable filename completion. + complete -c ~ts -f + + """, [Progname, Progname]), + + Chunk2 = completion_for_fish(Context, ArgparseDef, []), + + io:format("~ts~n~ts", [Chunk1, Chunk2]). + +completion_for_fish(Context, ArgparseDef, CmdPath) -> + Chunk1 = format_arguments_for_fish(Context, ArgparseDef, CmdPath), + Chunk2 = format_commands_for_fish(Context, ArgparseDef, CmdPath), + [Chunk1, Chunk2]. + +format_arguments_for_fish( + #rabbit_cli{progname = Progname}, + #{arguments := Arguments}, + CmdPath) when Arguments =/= [] -> + Chunk = lists:map( + fun(Arg) -> + Cond = case CmdPath of + [] -> + ""; + _ -> + format_cmdpath_cond_for_fish( + CmdPath, []) + end, + Option = format_arg_for_fish(Arg), + Desc = format_desc_for_fish(Arg), + io_lib:format( + "complete -c ~ts~ts~ts~ts~n", + [Progname, Cond, Option, Desc]) + end, Arguments), + [io_lib:nl(), Chunk]; +format_arguments_for_fish(_Context, _ArgparseDef, _CmdPath) -> + "". + +format_commands_for_fish( + #rabbit_cli{progname = Progname} = Context, + #{commands := Commands}, + CmdPath) when Commands =/= #{} -> + CmdNames = lists:sort(maps:keys(Commands)), + Chunk1 = lists:map( + fun(CmdName) -> + Command = maps:get(CmdName, Commands), + Cond = format_cmdpath_cond_for_fish(CmdPath, CmdNames), + Desc = format_desc_for_fish(Command), + io_lib:format( + "complete -c ~ts~ts -a ~ts~ts~n", + [Progname, Cond, CmdName, Desc]) + end, CmdNames), + Chunk2 = lists:map( + fun(CmdName) -> + Command = maps:get(CmdName, Commands), + completion_for_fish( + Context, Command, CmdPath ++ [CmdName]) + end, CmdNames), + [io_lib:nl(), Chunk1, Chunk2]; +format_commands_for_fish(_Context, _ArgparseDef, _CmdPath) -> + "". + +format_cmdpath_cond_for_fish([], _CmdNames) -> + " -n __fish_use_subcommand"; +format_cmdpath_cond_for_fish(CmdPath, CmdNames) -> + CondA = lists:map( + fun(CmdName) -> + io_lib:format( + "__fish_seen_subcommand_from ~ts", + [CmdName]) + end, CmdPath), + CondB = case CmdNames of + [] -> + []; + _ -> + CondB0 = lists:map( + fun(CmdName) -> + io_lib:format("~ts", [CmdName]) + end, CmdNames), + CondB1 = string:join(CondB0, " "), + CondB2 = io_lib:format( + "not __fish_seen_subcommand_from ~ts", + [CondB1]), + [CondB2] + end, + Cond1 = string:join(CondA ++ CondB, " && "), + Cond2 = lists:flatten(Cond1), + io_lib:format(" -n ~0p", [Cond2]). + +format_arg_for_fish(Arg) -> + Long = case Arg of + #{long := [$- | Name]} -> + io_lib:format(" -l ~ts", [Name]); + #{long := Name} -> + io_lib:format(" -o ~ts", [Name]); + _ -> + "" + end, + Short = case Arg of + #{short := Char} -> + io_lib:format(" -s ~tc", [Char]); + _ -> + "" + end, + IsRequired = case Arg of + #{required := R} -> + R; + #{long := _} -> + false; + #{short := _} -> + false; + _ -> + true + end, + Required = case IsRequired of + true -> + " -r"; + false -> + "" + end, + Type = maps:get(type, Arg, string), + ArgArg = case Type of + {ErlType, [H | _] = Choices} + when ErlType =:= atom orelse + ErlType =:= binary orelse + (ErlType =:= string andalso is_list(H)) -> + AA0 = lists:map( + fun(Choice) -> + io_lib:format("~ts", [Choice]) + end, Choices), + AA1 = string:join(AA0, " "), + AA2 = lists:flatten(AA1), + AA3 = io_lib:format(" -a ~p", [AA2]), + AA3; + _ -> + "" + end, + [Long, Short, Required, ArgArg]. + +format_desc_for_fish(#{help := Help}) -> + io_lib:format(" -d ~0p", [Help]); +format_desc_for_fish(_) -> + "". + %% ------------------------------------------------------------------- %% Helpers. %% ------------------------------------------------------------------- From 2e4075694a206be754cc2c93524018a2f9ad1901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 24 Jun 2025 16:20:22 +0200 Subject: [PATCH 34/51] Re-implement import/export definitions --- deps/rabbit/src/rabbit_cli_backend.erl | 37 +++++++++ deps/rabbit/src/rabbit_cli_commands.erl | 34 ++------ deps/rabbit/src/rabbit_cli_frontend.erl | 11 ++- deps/rabbit/src/rabbit_cli_http_client.erl | 18 ++++- deps/rabbit/src/rabbit_cli_http_listener.erl | 10 ++- deps/rabbit/src/rabbit_cli_io.erl | 69 ++++++++++------- deps/rabbit/src/rabbit_cli_transport2.erl | 8 +- deps/rabbit/src/rabbit_definitions.erl | 81 ++++++++++++++++++++ 8 files changed, 207 insertions(+), 61 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 636692a3ffcb..b433821f00e3 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -10,6 +10,9 @@ -include("src/rabbit_cli_backend.hrl"). -export([run_command/2, + read_stdin/1, + read_file/2, + write_file/3, start_link/3]). -export([init/1, callback_mode/0, @@ -44,6 +47,40 @@ start_link(Context, Caller, GroupLeader) -> group_leader => GroupLeader}, gen_statem:start_link(?MODULE, Args, []). +read_stdin(Context) -> + read_stdin(Context, []). + +read_stdin(Context, Data) -> + case io:get_chars("", 4096) of + Chunk when is_list(Chunk) -> + Data1 = [Data, Chunk], + read_stdin(Context, Data1); + eof -> + {ok, list_to_binary(Data)}; + {error, _} = Error -> + Error + end. + +read_file(Context, Filename) -> + Request = {read_file, Filename}, + send_frontend_request(Context, Request). + +write_file(Context, Filename, Bytes) -> + Request = {write_file, Filename, Bytes}, + send_frontend_request(Context, Request). + +send_frontend_request( + #rabbit_cli{priv = #?MODULE{caller = Caller}}, Request) -> + Mref = erlang:monitor(process, Caller), + Caller ! {frontend_request, {self(), Mref}, Request}, + receive + {Mref, Reply} -> + erlang:demonitor(Mref, [flush]), + Reply; + {'DOWN', Mref, _, _, Reason} -> + exit(Reason) + end. + %% ------------------------------------------------------------------- %% gen_statem callbacks. %% ------------------------------------------------------------------- diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 512c817f866e..487fbe609499 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -16,7 +16,6 @@ cmd_noop/1, cmd_hello/1, cmd_crash/1, - cmd_import_definitions/1, cmd_top/1]). -rabbitmq_command( @@ -50,12 +49,6 @@ ], handler => {?MODULE, cmd_declare_exchange}}}). --rabbitmq_command( - {#{cli => ["import", "definitions"]}, - [argparse_def_file_input, - #{help => "Import definitions", - handler => {?MODULE, cmd_import_definitions}}]}). - -rabbitmq_command( {#{cli => ["top"]}, [#{help => "Top-like interactive view", @@ -149,21 +142,19 @@ expand_argparse_def(Def) when is_map(Def) -> expand_argparse_def(Defs) when is_list(Defs) -> lists:foldl( fun - (argparse_def_record_stream, Acc) -> - Def = rabbit_cli_io:argparse_def(record_stream), - merge_argparse_def(Acc, Def); - (argparse_def_file_input, Acc) -> - Def = rabbit_cli_io:argparse_def(file_input), - merge_argparse_def(Acc, Def); (Mod, Acc) when is_atom(Mod) -> Def = Mod:argparse_def(), merge_argparse_def(Acc, Def); - (Def, Acc) -> + ({Mod, Function, Args}, Acc) when is_atom(Mod) -> + Def = erlang:apply(Mod, Function, Args), + merge_argparse_def(Acc, Def); + (Def, Acc) when is_map(Def) -> Def1 = expand_argparse_def(Def), merge_argparse_def(Acc, Def1) end, #{}, Defs). -merge_argparse_def(ArgparseDef1, ArgparseDef2) -> +merge_argparse_def(ArgparseDef1, ArgparseDef2) + when is_map(ArgparseDef1) andalso is_map(ArgparseDef2) -> Args1 = maps:get(arguments, ArgparseDef1, []), Args2 = maps:get(arguments, ArgparseDef2, []), Args = merge_arguments(Args1, Args2), @@ -393,19 +384,6 @@ cmd_hello(_) -> cmd_crash(_) -> erlang:exit(oops). -cmd_import_definitions(#{progname := Progname, arg_map := ArgMap}) -> - {ok, IO} = rabbit_cli_io:start_link(Progname), - %% TODO: Use a wrapper above `file' to proxy through transport. - Ret = case rabbit_cli_io:read_file(IO, ArgMap) of - {ok, Data} -> - rabbit_cli_io:format(IO, "Import definitions:~n ~s~n", [Data]), - ok; - {error, _} = Error -> - Error - end, - rabbit_cli_io:stop(IO), - Ret. - cmd_top(#{io := IO} = Context) -> Top = spawn_link(fun() -> run_top(IO) end), wait_quit(Context, Top). diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 6c254f20b307..3e392b50fe75 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -222,9 +222,13 @@ record_to_map([Field | Rest], Record, Index, Map) -> record_to_map([], _Record, _Index, Map) -> Map. -main_loop(#rabbit_cli{} = Context) -> +main_loop( + #rabbit_cli{priv = #?MODULE{connection = Connection}} = Context) -> ?LOG_DEBUG("CLI: frontend main loop..."), receive + {frontend_request, From, Request} -> + Reply = handle_request(Request), + rabbit_cli_transport2:gen_reply(Connection, From, Reply); {'EXIT', _LinkedPid, Reason} -> terminate(Reason, Context); Info -> @@ -235,3 +239,8 @@ main_loop(#rabbit_cli{} = Context) -> terminate(Reason, _Context) -> ?LOG_DEBUG("CLI: frontend terminating: ~0p", [Reason]), ok. + +handle_request({read_file, Filename}) -> + file:read_file(Filename); +handle_request({write_file, Filename, Bytes}) -> + file:write_file(Filename, Bytes). diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index 8590555fafc3..89d177e8e64e 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -5,7 +5,8 @@ -include_lib("kernel/include/logger.hrl"). -export([start_link/1, - run_command/2]). + run_command/2, + gen_reply/3]). -export([init/1, callback_mode/0, handle_event/4, @@ -17,19 +18,24 @@ stream :: gun:stream_ref(), delayed_requests = [] :: list(), io_requests = #{} :: map(), + caller :: pid(), group_leader :: pid()}). start_link(Uri) -> - gen_statem:start_link(?MODULE, Uri, []). + Caller = self(), + gen_statem:start_link(?MODULE, #{uri => Uri, caller => Caller}, []). run_command(Client, ContextMap) -> gen_statem:call(Client, {?FUNCTION_NAME, ContextMap}). +gen_reply(Client, From, Reply) -> + gen_statem:cast(Client, {?FUNCTION_NAME, From, Reply}). + %% ------------------------------------------------------------------- %% gen_statem callbacks. %% ------------------------------------------------------------------- -init(Uri) -> +init(#{uri := Uri, caller := Caller}) -> maybe #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), GroupLeader = erlang:group_leader(), @@ -40,6 +46,7 @@ init(Uri) -> {ok, ConnPid} ?= gun:open(Host, Port), Data = #?MODULE{uri = UriMap, + caller = Caller, group_leader = GroupLeader, connection = ConnPid}, {ok, opening_connection, Data} @@ -156,6 +163,11 @@ handle_request({call_ret, From, Reply}, Data) -> {noreply, Data}; handle_request({call_exception, Class, Reason, Stacktrace}, _Data) -> erlang:raise(Class, Reason, Stacktrace); +handle_request( + {frontend_request, _From, _Request} = FrontendRequest, + #?MODULE{caller = Caller} = Data) -> + Caller ! FrontendRequest, + {noreply, Data}; handle_request( {io_request, From, ReplyAs, Request}, #?MODULE{group_leader = GroupLeader, diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl index 4e99a6ec5559..43f9b35fc656 100644 --- a/deps/rabbit/src/rabbit_cli_http_listener.erl +++ b/deps/rabbit/src/rabbit_cli_http_listener.erl @@ -154,6 +154,10 @@ websocket_handle(Frame, State) -> ?LOG_DEBUG("CLI: unhandled Websocket frame: ~p", [Frame]), {ok, State}. +websocket_info({frontend_request, _From, _Request} = FrontendRequest, State) -> + FrontendRequestBin = term_to_binary(FrontendRequest), + Frame = {binary, FrontendRequestBin}, + {[Frame], State}; websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> IoRequestBin = term_to_binary(IoRequest), Frame = {binary, IoRequestBin}, @@ -194,4 +198,8 @@ handle_request({cast, Command}) -> handle_command({run_command, ContextMap}) -> Caller = self(), - rabbit_cli_backend:run_command(ContextMap, Caller). + rabbit_cli_backend:run_command(ContextMap, Caller); +handle_command({gen_reply, From, Reply}) -> + gen:reply(From, Reply); +handle_command({send, Dest, Msg}) -> + erlang:send(Dest, Msg). diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 4104963f78f3..4192eaa81289 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -26,46 +26,61 @@ kbd_reader = undefined, kbd_subscribers = []}). -start_link(Progname) -> - gen_server:start_link(rabbit_cli_io, #{progname => Progname}, []). - -stop(IO) -> - MRef = erlang:monitor(process, IO), - _ = gen_server:call(IO, stop), - receive - {'DOWN', MRef, _, _, _Reason} -> - ok - end. - -argparse_def(record_stream) -> +% argparse_def(record_stream) -> +% #{arguments => +% [ +% #{name => output, +% long => "-output", +% short => $o, +% type => string, +% nargs => 1, +% help => "Write output to file "}, +% #{name => format, +% long => "-format", +% short => $f, +% type => {atom, [plain, json]}, +% default => plain, +% help => "Format output acccording to "} +% ] +% }; +argparse_def(file_input) -> #{arguments => [ - #{name => output, - long => "-output", - short => $o, + #{name => input, + long => "-input", + short => $i, type => string, nargs => 1, - help => "Write output to file "}, - #{name => format, - long => "-format", - short => $f, - type => {atom, [plain, json]}, - default => plain, - help => "Format output acccording to "} + help => "Read input from file "} ] }; -argparse_def(file_input) -> +argparse_def(file_output) -> #{arguments => [ - #{name => input, - long => "-input", - short => $i, + #{name => output, + long => "-output", + short => $o, type => string, nargs => 1, - help => "Read input from file "} + help => "Write output to file "} ] }. +%% ------------------------------------------------------------------- +%% OLD CODE (to remove). +%% ------------------------------------------------------------------- + +start_link(Progname) -> + gen_server:start_link(rabbit_cli_io, #{progname => Progname}, []). + +stop(IO) -> + MRef = erlang:monitor(process, IO), + _ = gen_server:call(IO, stop), + receive + {'DOWN', MRef, _, _, _Reason} -> + ok + end. + display_help(#{io := {transport, Transport}} = Context) -> Transport ! {io_cast, {?FUNCTION_NAME, Context}}; display_help(#{io := IO} = Context) -> diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index d2d9ce2c3af4..fca643d91791 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -3,7 +3,8 @@ -include_lib("kernel/include/logger.hrl"). -export([connect/0, connect/1, - run_command/2]). + run_command/2, + gen_reply/3]). -record(?MODULE, {type :: erldist | http, peer :: atom() | pid()}). @@ -89,3 +90,8 @@ run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> rabbit_cli_http_client:run_command(Client, ContextMap). + +gen_reply(#?MODULE{type = erldist}, From, Reply) -> + gen:reply(From, Reply); +gen_reply(#?MODULE{type = http, peer = Client}, From, Reply) -> + rabbit_cli_http_client:gen_reply(Client, From, Reply). diff --git a/deps/rabbit/src/rabbit_definitions.erl b/deps/rabbit/src/rabbit_definitions.erl index 884466a81787..c312edc04726 100644 --- a/deps/rabbit/src/rabbit_definitions.erl +++ b/deps/rabbit/src/rabbit_definitions.erl @@ -29,8 +29,12 @@ %% * rabbit_definitions_import_http %% * rabbit_definitions_hashing -module(rabbit_definitions). + +-include_lib("kernel/include/logger.hrl"). -include_lib("rabbit_common/include/rabbit.hrl"). +-include("src/rabbit_cli_backend.hrl"). + -export([boot/0]). %% automatic import on boot -export([ @@ -56,6 +60,10 @@ ]). -export([decode/1, decode/2, args/1, validate_definitions/1]). +%% CLI commands. +-export([cmd_import_definitions/1, + cmd_export_definitions/1]). + %% for tests -export([ maybe_load_definitions_from_local_filesystem_if_unchanged/3, @@ -90,6 +98,18 @@ -export_type([definition_object/0, definition_list/0, definition_category/0, definitions/0]). +-rabbitmq_command( + {#{cli => ["import", "definitions"]}, + [{rabbit_cli_io, argparse_def, [file_input]}, + #{help => "Import definitions", + handler => {?MODULE, cmd_import_definitions}}]}). + +-rabbitmq_command( + {#{cli => ["export", "definitions"]}, + [{rabbit_cli_io, argparse_def, [file_output]}, + #{help => "Export definitions", + handler => {?MODULE, cmd_export_definitions}}]}). + -define(IMPORT_WORK_POOL, definition_import_pool). boot() -> @@ -1159,3 +1179,64 @@ topic_permission_definition(P0) -> tags_as_binaries(Tags) -> [to_binary(T) || T <- Tags]. + +%% ------------------------------------------------------------------- +%% CLI commands. +%% ------------------------------------------------------------------- + +cmd_import_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> + case ArgMap of + #{input := "-"} -> + import_from_stdin(Context); + #{input := Filename} -> + import_from_file(Context, Filename); + _ -> + import_from_stdin(Context) + end. + +import_from_file(Context, Filename) -> + case rabbit_cli_backend:read_file(Context, Filename) of + {ok, Data} -> + do_import(Context, Data); + {error, _} = Error -> + Error + end. + +import_from_stdin(Context) -> + case rabbit_cli_backend:read_stdin(Context) of + {ok, Data} -> + do_import(Context, Data); + {error, _} = Error -> + Error + end. + +do_import(_Context, Data) -> + try + Json = json:decode(Data), + import_parsed(Json) + catch + error:unexpected_end = Reason -> + {error, Reason}; + error:{invalid_byte, _Byte} = Reason -> + {error, Reason}; + error:{unexpected_sequence, _Bytes} = Reason -> + {error, Reason} + end. + +cmd_export_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> + Defs = all_definitions(), + Json = json:encode(Defs), + case ArgMap of + #{output := "-"} -> + export_to_stdin(Context, Json); + #{output := Filename} -> + export_to_file(Context, Json, Filename); + _ -> + export_to_stdin(Context, Json) + end. + +export_to_file(Context, Json, Filename) -> + rabbit_cli_backend:write_file(Context, Filename, Json). + +export_to_stdin(_Context, Json) -> + io:format("~ts~n", [Json]). From 4af65f205579fdb607600df54286ca8b6fb44a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 24 Jun 2025 17:09:10 +0200 Subject: [PATCH 35/51] Distinguish legacy tools and commands --- deps/rabbit/src/rabbit_cli_backend.erl | 53 +++++++++++++++++++++++-- deps/rabbit/src/rabbit_cli_backend.hrl | 1 + deps/rabbit/src/rabbit_cli_commands.erl | 22 +--------- deps/rabbit/src/rabbit_cli_datagrid.erl | 36 ++++++++++------- deps/rabbit/src/rabbit_cli_frontend.erl | 5 ++- deps/rabbit/src/rabbit_exchange.erl | 30 ++++++++++++-- 6 files changed, 101 insertions(+), 46 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index b433821f00e3..450301bae790 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -34,12 +34,30 @@ run_command(#rabbit_cli{} = Context, Caller) when is_pid(Caller) -> rabbit_cli_backend_sup:start_backend(Context, Caller, GroupLeader). map_to_context(ContextMap) -> - #rabbit_cli{progname = maps:get(progname, ContextMap), + Progname = maps:get(progname, ContextMap), + Legacy = is_legacy_progname(Progname), + #rabbit_cli{progname = Progname, args = maps:get(args, ContextMap), argparse_def = maps:get(argparse_def, ContextMap), arg_map = maps:get(arg_map, ContextMap), cmd_path = maps:get(cmd_path, ContextMap), - command = maps:get(command, ContextMap)}. + command = maps:get(command, ContextMap), + legacy = Legacy}. + +is_legacy_progname(<<"rabbitmqctl">>) -> + true; +is_legacy_progname(<<"rabbitmq-diagnostics">>) -> + true; +is_legacy_progname(<<"rabbitmq-plugins">>) -> + true; +is_legacy_progname(<<"rabbitmq-queues">>) -> + true; +is_legacy_progname(<<"rabbitmq-streams">>) -> + true; +is_legacy_progname(<<"rabbitmq-upgrade">>) -> + true; +is_legacy_progname(_Progname) -> + false. start_link(Context, Caller, GroupLeader) -> Args = #{context => Context, @@ -133,11 +151,38 @@ code_change(_Vsn, State, Data, _Extra) -> %% ------------------------------------------------------------------- final_argparse_def( - #rabbit_cli{argparse_def = PartialArgparseDef}) -> + #rabbit_cli{argparse_def = PartialArgparseDef} = Context) -> FullArgparseDef = rabbit_cli_commands:discovered_argparse_def(), ArgparseDef1 = rabbit_cli_commands:merge_argparse_def( PartialArgparseDef, FullArgparseDef), - ArgparseDef1. + ArgparseDef2 = filter_legacy_commands(Context, ArgparseDef1), + ArgparseDef2. + +filter_legacy_commands(#rabbit_cli{legacy = Legacy} = Context, ArgparseDef) -> + case ArgparseDef of + #{commands := Commands} -> + Commands1 = ( + maps:filtermap( + fun(_CmdName, Command) -> + Keep = (Legacy =:= is_legacy_command(Command)), + case Keep of + true -> + Command1 = filter_legacy_commands( + Context, Command), + {true, Command1}; + false -> + false + end + end, Commands)), + ArgparseDef#{commands => Commands1}; + _ -> + ArgparseDef + end. + +is_legacy_command(#{legacy := true}) -> + true; +is_legacy_command(_Command) -> + false. final_parse( #rabbit_cli{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index c52d5d8de3bb..b0274124e47f 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -4,5 +4,6 @@ arg_map, cmd_path, command, + legacy, priv}). diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 487fbe609499..c0cbe4a8df4f 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -10,8 +10,7 @@ -export([discover_commands/0, discovered_commands/0, discovered_argparse_def/0, - merge_argparse_def/2, - expect_legacy/1]). + merge_argparse_def/2]). -export([cmd_generate_completion_script/1, cmd_noop/1, cmd_hello/1, @@ -350,25 +349,6 @@ format_desc_for_fish(#{help := Help}) -> format_desc_for_fish(_) -> "". -%% ------------------------------------------------------------------- -%% Helpers. -%% ------------------------------------------------------------------- - -expect_legacy(#rabbit_cli{progname = <<"rabbitmqctl">>}) -> - true; -expect_legacy(#rabbit_cli{progname = <<"rabbitmq-diagnostics">>}) -> - true; -expect_legacy(#rabbit_cli{progname = <<"rabbitmq-plugins">>}) -> - true; -expect_legacy(#rabbit_cli{progname = <<"rabbitmq-queues">>}) -> - true; -expect_legacy(#rabbit_cli{progname = <<"rabbitmq-streams">>}) -> - true; -expect_legacy(#rabbit_cli{progname = <<"rabbitmq-upgrade">>}) -> - true; -expect_legacy(_Context) -> - false. - %% ------------------------------------------------------------------- %% XXX %% ------------------------------------------------------------------- diff --git a/deps/rabbit/src/rabbit_cli_datagrid.erl b/deps/rabbit/src/rabbit_cli_datagrid.erl index acd6c81c5f6b..70f61ab703d4 100644 --- a/deps/rabbit/src/rabbit_cli_datagrid.erl +++ b/deps/rabbit/src/rabbit_cli_datagrid.erl @@ -93,33 +93,39 @@ format_fields(#?MODULE{fields = Fields} = State) -> {ok, State}. format_record(Record, #?MODULE{fields = Fields} = State) -> - Values1 = format_values(Fields, Record), + Values1 = format_values(Fields, Record, State), Values2 = string:join(Values1, "\t"), io:format("~ts~n", [Values2]), {ok, State}. -format_values(Fields, Values) -> - format_values(Fields, Values, []). +format_values(Fields, Values, State) -> + format_values(Fields, Values, State, []). -format_values([#{type := string} | Rest1], [Value | Rest2], Acc) -> +format_values([#{type := string} | Rest1], [Value | Rest2], State, Acc) -> String = io_lib:format("~ts", [Value]), Acc1 = [String | Acc], - format_values(Rest1, Rest2, Acc1); -format_values([#{type := binary} | Rest1], [Value | Rest2], Acc) -> + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := binary} | Rest1], [Value | Rest2], State, Acc) -> String = io_lib:format("~-20.. ts", [Value]), Acc1 = [String | Acc], - format_values(Rest1, Rest2, Acc1); -format_values([#{type := integer} | Rest1], [Value | Rest2], Acc) -> + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := integer} | Rest1], [Value | Rest2], State, Acc) -> String = io_lib:format("~b", [Value]), Acc1 = [String | Acc], - format_values(Rest1, Rest2, Acc1); -format_values([#{type := boolean} | Rest1], [Value | Rest2], Acc) -> - String = io_lib:format("~ts", [if Value -> "☑"; true -> "☐" end]), + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := boolean} | Rest1], [Value | Rest2], State, Acc) -> + #?MODULE{context = #rabbit_cli{legacy = Legacy}} = State, + String = case Legacy of + false -> + io_lib:format("~ts", [if Value -> "☑"; true -> "☐" end]); + true -> + io_lib:format("~ts", [Value]) + end, Acc1 = [String | Acc], - format_values(Rest1, Rest2, Acc1); -format_values([#{type := term} | Rest1], [Value | Rest2], Acc) -> + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := term} | Rest1], [Value | Rest2], State, Acc) -> String = io_lib:format("~0p", [Value]), Acc1 = [String | Acc], - format_values(Rest1, Rest2, Acc1); -format_values([], [], Acc) -> + format_values(Rest1, Rest2, State, Acc1); +format_values([], [], _State, Acc) -> lists:reverse(Acc). diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 3e392b50fe75..c5001695d353 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -59,9 +59,10 @@ flush_log_messages() -> %% ------------------------------------------------------------------- run_cli(ScriptName, Args) -> - ProgName = filename:basename(ScriptName, ".escript"), + ProgName0 = filename:basename(ScriptName, ".bat"), + ProgName1 = filename:basename(ProgName0, ".escript"), Priv = #?MODULE{scriptname = ScriptName}, - Context = #rabbit_cli{progname = list_to_binary(ProgName), + Context = #rabbit_cli{progname = list_to_binary(ProgName1), args = Args, priv = Priv}, init_local_args(Context). diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index 71d1cba8a9ac..995a8da756e0 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -44,8 +44,7 @@ -define(DEFAULT_EXCHANGE_NAME, <<>>). -rabbitmq_command( - {#{cli => ["list", "exchanges"], - http => {get, ["exchanges"]}}, + {#{cli => ["list", "exchanges"]}, [rabbit_cli_datagrid, #{help => "List exchanges", arguments => @@ -58,6 +57,21 @@ help => "Fields to include"} ], handler => {?MODULE, cmd_list_exchanges}}]}). +-rabbitmq_command( + {#{cli => ["list_exchanges"]}, + [rabbit_cli_datagrid, + #{help => "List exchanges", + arguments => + [ + #{name => fields, + %% FIXME: Exclude user_who_performed_action + type => {atom, ?INFO_KEYS}, + nargs => list, + required => false, + help => "Fields to include"} + ], + handler => {?MODULE, cmd_list_exchanges}, + legacy => true}]}). -spec recover(rabbit_types:vhost()) -> [name()]. @@ -587,8 +601,9 @@ type_to_route_fun(T) -> %% CLI commands. %% ------------------------------------------------------------------- -cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap} = Context) -> - InfoKeys = rabbit_exchange:info_keys() -- [user_who_performed_action], +cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap, legacy = Legacy} = Context) -> + VHost = <<"/">>, %% TODO: Take from args and use it. + InfoKeys = rabbit_exchange:info_keys(), Fields0 = case ArgMap of #{fields := Arg} -> Arg; @@ -601,6 +616,13 @@ cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap} = Context) -> end, Fields0), Priv = #{fields => Fields1}, + case Legacy of + false -> + ok; + true -> + io:format("Listing exchanges for vhost ~ts ...~n", [VHost]) + end, + %% Start datagrid with callbacks. rabbit_cli_datagrid:process( fun exchanges_fields/1, From f614d8f6b453631e2cde2ad0f507abccca3a730f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 24 Jun 2025 17:35:50 +0200 Subject: [PATCH 36/51] Fix Dialyzer errors --- deps/rabbit/src/rabbit_cli_backend.erl | 12 ++++++------ deps/rabbit/src/rabbit_cli_backend.hrl | 14 +++++++------- deps/rabbit/src/rabbit_cli_commands.erl | 4 +++- deps/rabbit/src/rabbit_cli_frontend.erl | 10 +++++++--- deps/rabbit/src/rabbit_cli_http_client.erl | 8 ++++---- deps/rabbit/src/rabbit_cli_http_listener.erl | 8 ++++---- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 450301bae790..98267cf96873 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -44,17 +44,17 @@ map_to_context(ContextMap) -> command = maps:get(command, ContextMap), legacy = Legacy}. -is_legacy_progname(<<"rabbitmqctl">>) -> +is_legacy_progname("rabbitmqctl") -> true; -is_legacy_progname(<<"rabbitmq-diagnostics">>) -> +is_legacy_progname("rabbitmq-diagnostics") -> true; -is_legacy_progname(<<"rabbitmq-plugins">>) -> +is_legacy_progname("rabbitmq-plugins") -> true; -is_legacy_progname(<<"rabbitmq-queues">>) -> +is_legacy_progname("rabbitmq-queues") -> true; -is_legacy_progname(<<"rabbitmq-streams">>) -> +is_legacy_progname("rabbitmq-streams") -> true; -is_legacy_progname(<<"rabbitmq-upgrade">>) -> +is_legacy_progname("rabbitmq-upgrade") -> true; is_legacy_progname(_Progname) -> false. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index b0274124e47f..c6eff6f2b44f 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -1,9 +1,9 @@ --record(rabbit_cli, {progname, - args, - argparse_def, - arg_map, - cmd_path, - command, - legacy, +-record(rabbit_cli, {progname :: string(), + args :: argparse:args(), + argparse_def :: argparse:command() | undefined, + arg_map :: argparse:arg_map() | undefined, + cmd_path :: argparse:cmd_path() | undefined, + command :: argparse:command() | undefined, + legacy :: boolean() | undefined, priv}). diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index c0cbe4a8df4f..7d81ca78d6d1 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -361,6 +361,8 @@ cmd_hello(_) -> io:format("Hello ~s!~n", [string:trim(Name)]), ok. +-spec cmd_crash(#rabbit_cli{}) -> no_return(). + cmd_crash(_) -> erlang:exit(oops). @@ -373,7 +375,7 @@ run_top(IO) -> quit -> ok after 1000 -> - rabbit_cli_io:format(IO, "Refresh~n", []), + _ = rabbit_cli_io:format(IO, "Refresh~n", []), run_top(IO) end. diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index c5001695d353..973fb83dfc19 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -11,6 +11,9 @@ -record(?MODULE, {scriptname, connection}). +-spec main(Args) -> no_return() when + Args :: argparse:args(). + main(Args) -> ScriptName = escript:script_name(), add_rabbitmq_code_path(ScriptName), @@ -52,7 +55,8 @@ configure_logging() -> ok. flush_log_messages() -> - rabbit_logger_std_h:filesync(?LOG_HANDLER_NAME). + _ = rabbit_logger_std_h:filesync(?LOG_HANDLER_NAME), + ok. %% ------------------------------------------------------------------- %% Preparation for remote command execution. @@ -62,7 +66,7 @@ run_cli(ScriptName, Args) -> ProgName0 = filename:basename(ScriptName, ".bat"), ProgName1 = filename:basename(ProgName0, ".escript"), Priv = #?MODULE{scriptname = ScriptName}, - Context = #rabbit_cli{progname = list_to_binary(ProgName1), + Context = #rabbit_cli{progname = ProgName1, args = Args, priv = Priv}, init_local_args(Context). @@ -84,7 +88,7 @@ init_local_args(Context) -> set_log_level(#rabbit_cli{arg_map = #{verbose := Verbosity}} = Context) when Verbosity >= 3 -> - logger:set_primary_config(level, debug), + _ = logger:set_primary_config(level, debug), connect_to_node(Context); set_log_level(#rabbit_cli{} = Context) -> connect_to_node(Context). diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index 89d177e8e64e..d20a795cc118 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -15,7 +15,7 @@ -record(?MODULE, {uri :: uri_string:uri_map(), connection :: pid(), - stream :: gun:stream_ref(), + stream :: gun:stream_ref() | undefined, delayed_requests = [] :: list(), io_requests = #{} :: map(), caller :: pid(), @@ -90,9 +90,9 @@ handle_event( Request = binary_to_term(RequestBin), ?LOG_DEBUG("CLI: received HTTP message from server: ~p", [Request]), case handle_request(Request, Data) of - {reply, Reply, Data1} -> - send_request(Reply, Data1), - {keep_state, Data1}; + % {reply, Reply, Data1} -> + % send_request(Reply, Data1), + % {keep_state, Data1}; {noreply, Data1} -> {keep_state, Data1}; {stop, Reason} -> diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl index 43f9b35fc656..9cf919107a4e 100644 --- a/deps/rabbit/src/rabbit_cli_http_listener.erl +++ b/deps/rabbit/src/rabbit_cli_http_listener.erl @@ -18,7 +18,9 @@ websocket_info/2, terminate/3]). --record(?MODULE, {listeners = [] :: [pid()]}). +-record(?MODULE, {listeners = [] :: [{proto(), inet:port_number(), pid()}]}). + +-type proto() :: erldist | http. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, #{}, []). @@ -34,9 +36,7 @@ init(_) -> ignore; {ok, Listeners} -> State = #?MODULE{listeners = Listeners}, - {ok, State, hibernate}; - {error, _} = Error -> - Error + {ok, State, hibernate} end. handle_call(Request, From, State) -> From 8833b9d696f58bada1afb3de672e0f9aac1b3cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 24 Jun 2025 18:02:31 +0200 Subject: [PATCH 37/51] Detect is std* is a terminal or not --- deps/rabbit/src/rabbit_cli_backend.erl | 18 ++++++++++++++++-- deps/rabbit/src/rabbit_cli_backend.hrl | 4 ++++ deps/rabbit/src/rabbit_cli_frontend.erl | 5 +++++ deps/rabbit/src/rabbit_definitions.erl | 18 ++++++++++++------ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 98267cf96873..5260890b9032 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -42,7 +42,8 @@ map_to_context(ContextMap) -> arg_map = maps:get(arg_map, ContextMap), cmd_path = maps:get(cmd_path, ContextMap), command = maps:get(command, ContextMap), - legacy = Legacy}. + legacy = Legacy, + terminal = maps:get(terminal, ContextMap)}. is_legacy_progname("rabbitmqctl") -> true; @@ -103,10 +104,23 @@ send_frontend_request( %% gen_statem callbacks. %% ------------------------------------------------------------------- -init(#{context := Context, caller := Caller, group_leader := GroupLeader}) -> +init( + #{context := #rabbit_cli{progname = Progname, + args = Args, + terminal = Terminal} = Context, + caller := Caller, + group_leader := GroupLeader + }) -> process_flag(trap_exit, true), erlang:link(Caller), erlang:group_leader(GroupLeader, self()), + ?LOG_INFO("CLI: running: ~0p", [[Progname | Args]]), + ?LOG_DEBUG( + "CLI: tty: stdout=~s stderr=~s stdin=~s", + [maps:get(stdout, Terminal), + maps:get(stderr, Terminal), + maps:get(stdin, Terminal)]), + Priv = #?MODULE{caller = Caller}, Context1 = Context#rabbit_cli{priv = Priv}, {ok, standing_by, Context1, {next_event, internal, parse_command}}. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index c6eff6f2b44f..55afb8420ac7 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -6,4 +6,8 @@ command :: argparse:command() | undefined, legacy :: boolean() | undefined, + terminal :: #{stdout := boolean(), + stderr := boolean(), + stdin := boolean()}, + priv}). diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 973fb83dfc19..320adcb85ebb 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -66,8 +66,13 @@ run_cli(ScriptName, Args) -> ProgName0 = filename:basename(ScriptName, ".bat"), ProgName1 = filename:basename(ProgName0, ".escript"), Priv = #?MODULE{scriptname = ScriptName}, + IoOpts = io:getopts(), + Terminal = #{stdout => proplists:get_value(stdout, IoOpts), + stderr => proplists:get_value(stderr, IoOpts), + stdin => proplists:get_value(stdin, IoOpts)}, Context = #rabbit_cli{progname = ProgName1, args = Args, + terminal = Terminal, priv = Priv}, init_local_args(Context). diff --git a/deps/rabbit/src/rabbit_definitions.erl b/deps/rabbit/src/rabbit_definitions.erl index c312edc04726..87449eaa71db 100644 --- a/deps/rabbit/src/rabbit_definitions.erl +++ b/deps/rabbit/src/rabbit_definitions.erl @@ -1225,18 +1225,24 @@ do_import(_Context, Data) -> cmd_export_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> Defs = all_definitions(), - Json = json:encode(Defs), case ArgMap of #{output := "-"} -> - export_to_stdin(Context, Json); + export_to_stdin(Context, Defs); #{output := Filename} -> - export_to_file(Context, Json, Filename); + export_to_file(Context, Defs, Filename); _ -> - export_to_stdin(Context, Json) + export_to_stdin(Context, Defs) end. -export_to_file(Context, Json, Filename) -> +export_to_file(Context, Defs, Filename) -> + Json = json:encode(Defs), rabbit_cli_backend:write_file(Context, Filename, Json). -export_to_stdin(_Context, Json) -> +export_to_stdin(#rabbit_cli{terminal = Terminal}, Defs) -> + Json = case Terminal of + #{stdout := true} -> + json:format(Defs); + _ -> + json:encode(Defs) + end, io:format("~ts~n", [Json]). From ecfd43fc0aaf276293c40589147df943a5a1f240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 24 Jun 2025 19:04:58 +0200 Subject: [PATCH 38/51] Pass env + terminfo --- deps/rabbit/Makefile | 4 +++- deps/rabbit/src/rabbit_cli_backend.erl | 2 ++ deps/rabbit/src/rabbit_cli_backend.hrl | 7 ++++++- deps/rabbit/src/rabbit_cli_frontend.erl | 28 +++++++++++++++++++++---- deps/rabbit/src/rabbit_cli_io.erl | 21 ++++++++++++++++++- deps/rabbit/src/rabbit_exchange.erl | 17 ++++++++++++++- 6 files changed, 71 insertions(+), 8 deletions(-) diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index db5d39a9cec9..466517503ff5 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -129,7 +129,7 @@ endef LOCAL_DEPS = sasl os_mon inets compiler public_key crypto ssl syntax_tools xmerl BUILD_DEPS = rabbitmq_cli -DEPS = ranch cowlib rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat horus khepri khepri_mnesia_migration cuttlefish gen_batch_server cowboy gun +DEPS = ranch cowlib rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat horus khepri khepri_mnesia_migration cuttlefish gen_batch_server cowboy gun eterminfo TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_client rabbitmq_amqp_client rabbitmq_amqp1_0 # We pin a version of Horus even if we don't use it directly (it is a @@ -138,6 +138,8 @@ TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_clie # should be removed with the next update of Khepri. dep_horus = hex 0.3.1 +dep_eterminfo = git https://github.com/tomas-abrahamsson/eterminfo.git 2.0 + PLT_APPS += mnesia runtime_tools dep_syslog = git https://github.com/schlagert/syslog 4.0.0 diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 5260890b9032..ae7306785204 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -43,6 +43,8 @@ map_to_context(ContextMap) -> cmd_path = maps:get(cmd_path, ContextMap), command = maps:get(command, ContextMap), legacy = Legacy, + os = maps:get(os, ContextMap), + env = maps:get(env, ContextMap), terminal = maps:get(terminal, ContextMap)}. is_legacy_progname("rabbitmqctl") -> diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index 55afb8420ac7..7dab373a56a7 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -6,8 +6,13 @@ command :: argparse:command() | undefined, legacy :: boolean() | undefined, + os :: {unix | win32, atom()}, + env :: [{os:env_var_name(), os:env_var_value()}], terminal :: #{stdout := boolean(), stderr := boolean(), - stdin := boolean()}, + stdin := boolean(), + + name := eterminfo:term_name(), + info := eterminfo:terminfo()}, priv}). diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 320adcb85ebb..d117eca03c61 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -65,17 +65,37 @@ flush_log_messages() -> run_cli(ScriptName, Args) -> ProgName0 = filename:basename(ScriptName, ".bat"), ProgName1 = filename:basename(ProgName0, ".escript"), + Terminal = collect_terminal_info(), Priv = #?MODULE{scriptname = ScriptName}, - IoOpts = io:getopts(), - Terminal = #{stdout => proplists:get_value(stdout, IoOpts), - stderr => proplists:get_value(stderr, IoOpts), - stdin => proplists:get_value(stdin, IoOpts)}, Context = #rabbit_cli{progname = ProgName1, args = Args, + os = os:type(), + env = os:env(), terminal = Terminal, priv = Priv}, init_local_args(Context). +collect_terminal_info() -> + IoOpts = io:getopts(), + Term = eterminfo:get_term_type_or_default(), + TermInfo = case eterminfo:read_by_infocmp(Term) of + {ok, TI} -> + TI; + _ -> + case eterminfo:read_by_file(Term) of + {ok, TI} -> + TI; + _ -> + undefined + end + end, + #{stdout => proplists:get_value(stdout, IoOpts), + stderr => proplists:get_value(stderr, IoOpts), + stdin => proplists:get_value(stdin, IoOpts), + + name => Term, + info => TermInfo}. + init_local_args(Context) -> maybe LocalArgparseDef = initial_argparse_def(), diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 4192eaa81289..bc8c76082f2b 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -4,9 +4,12 @@ -include_lib("rabbit_common/include/resource.hrl"). +-include("src/rabbit_cli_backend.hrl"). + +-export([argparse_def/1, + supports_colors/1]). -export([start_link/1, stop/1, - argparse_def/1, display_help/1, format/3, start_record_stream/4, @@ -66,6 +69,22 @@ argparse_def(file_output) -> ] }. +supports_colors(#rabbit_cli{env = Env, terminal = #{info := TermInfo}}) -> + ?LOG_DEBUG("Env: ~p", [Env]), + case proplists:get_value("COLORTERM", Env) of + "truecolor" -> + {true, truecolor}; + "24bit" -> + {true, truecolor}; + _ -> + case eterminfo:tigetnum_m(TermInfo, colors) of + Colors when is_integer(Colors) andalso Colors >= 0 -> + {true, Colors}; + _ -> + false + end + end. + %% ------------------------------------------------------------------- %% OLD CODE (to remove). %% ------------------------------------------------------------------- diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index 995a8da756e0..eaa27b308cf2 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -618,7 +618,22 @@ cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap, legacy = Legacy} = Context) -> case Legacy of false -> - ok; + case rabbit_cli_io:supports_colors(Context) of + {true, truecolor} -> + io:format( + "Listing \033[;2;193;18;31mexchanges\033[0m " + "for vhost \033[;2;102;155;188m~ts\033[0m:~n", + [VHost]); + {true, _} -> + io:format( + "Listing \033[1mexchanges\033[0m " + "for vhost \033[1m~ts\033[0m:~n", + [VHost]); + false -> + io:format( + "Listing exchanges for vhost ~ts:~n", + [VHost]) + end; true -> io:format("Listing exchanges for vhost ~ts ...~n", [VHost]) end, From ad66f76fe4340caaab18d250ffcba08f1537007d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 25 Jun 2025 13:51:32 +0200 Subject: [PATCH 39/51] Fix main loop aborting after one frontend request --- deps/rabbit/src/rabbit_cli_frontend.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index d117eca03c61..b521af5d8e47 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -256,11 +256,12 @@ main_loop( #rabbit_cli{priv = #?MODULE{connection = Connection}} = Context) -> ?LOG_DEBUG("CLI: frontend main loop..."), receive - {frontend_request, From, Request} -> - Reply = handle_request(Request), - rabbit_cli_transport2:gen_reply(Connection, From, Reply); {'EXIT', _LinkedPid, Reason} -> terminate(Reason, Context); + {frontend_request, From, Request} -> + Reply = handle_request(Request), + _ = rabbit_cli_transport2:gen_reply(Connection, From, Reply), + main_loop(Context); Info -> ?LOG_DEBUG("Unknown info: ~0p", [Info]), main_loop(Context) From f1f5fafb25ec57a18d5cc7762e71981517be2944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Wed, 25 Jun 2025 13:52:09 +0200 Subject: [PATCH 40/51] Support interactive command --- deps/rabbit/src/rabbit_cli_backend.erl | 26 +------ deps/rabbit/src/rabbit_cli_commands.erl | 99 ++++++++++++++++++++++++- deps/rabbit/src/rabbit_cli_frontend.erl | 6 +- deps/rabbit/src/rabbit_cli_io.erl | 92 +++++++++++++++-------- 4 files changed, 165 insertions(+), 58 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index ae7306785204..c5aaeca5cf8a 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -10,9 +10,7 @@ -include("src/rabbit_cli_backend.hrl"). -export([run_command/2, - read_stdin/1, - read_file/2, - write_file/3, + send_frontend_request/2, start_link/3]). -export([init/1, callback_mode/0, @@ -68,28 +66,6 @@ start_link(Context, Caller, GroupLeader) -> group_leader => GroupLeader}, gen_statem:start_link(?MODULE, Args, []). -read_stdin(Context) -> - read_stdin(Context, []). - -read_stdin(Context, Data) -> - case io:get_chars("", 4096) of - Chunk when is_list(Chunk) -> - Data1 = [Data, Chunk], - read_stdin(Context, Data1); - eof -> - {ok, list_to_binary(Data)}; - {error, _} = Error -> - Error - end. - -read_file(Context, Filename) -> - Request = {read_file, Filename}, - send_frontend_request(Context, Request). - -write_file(Context, Filename, Bytes) -> - Request = {write_file, Filename, Bytes}, - send_frontend_request(Context, Request). - send_frontend_request( #rabbit_cli{priv = #?MODULE{caller = Caller}}, Request) -> Mref = erlang:monitor(process, Caller), diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 7d81ca78d6d1..382d94e59696 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -356,11 +356,108 @@ format_desc_for_fish(_) -> cmd_noop(_) -> ok. -cmd_hello(_) -> +cmd_hello(Context) -> + io:format("Regural prompt test; type Enter to submit~n"), Name = io:get_line("Name: "), io:format("Hello ~s!~n", [string:trim(Name)]), + + io:format( + "~nInteraction mode test; " + "type any arrow keys, click with your mouse, or any other key to quit~n"), + ok = rabbit_cli_io:set_interactive_mode(Context), + io:format("\e[?1003h\e[?1015h\e[?1006h"), + Ret = cmd_hello_loop(Context), + io:format("\e[?1000l"), + Ret. + +cmd_hello_loop(Context) -> + case io:get_chars("", 1024) of + Chars when is_list(Chars) -> + case handle_hello_chars(Chars, Context) of + ok -> + cmd_hello_loop(Context); + stop -> + ok + end; + eof -> + ok; + {error, _} = Error -> + Error + end. + +handle_hello_chars("\e[A" ++ Rest, Context) -> + io:format("Key: Up\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[B" ++ Rest, Context) -> + io:format("Key: Down\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[C" ++ Rest, Context) -> + io:format("Key: Right\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[D" ++ Rest, Context) -> + io:format("Key: Left\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[<" ++ Rest, Context) -> + %% Mouse interaction. + {Mouse, Rest1} = split_escape_sequence(Rest, ""), + Attrs = string:lexemes(Mouse, ";"), + case Attrs of + ["35", Col0, Line0] -> + {Line, _} = string:to_integer(Line0), + {Col, ""} = string:to_integer(Col0), + io:format("Mouse: Move, line ~b, column ~b\r~n", [Line, Col]); + ["0", Col0, LineAndButton] -> + {Line, Button} = string:to_integer(LineAndButton), + {Col, ""} = string:to_integer(Col0), + case Button of + "M" -> + io:format("Mouse: Left button down, line ~b, column ~b\r~n", [Line, Col]); + "m" -> + io:format("Mouse: Left button up, line ~b, column ~b\r~n", [Line, Col]) + end; + ["1", Col0, LineAndButton] -> + {Line, Button} = string:to_integer(LineAndButton), + {Col, ""} = string:to_integer(Col0), + case Button of + "M" -> + io:format("Mouse: Middle button down, line ~b, column ~b\r~n", [Line, Col]); + "m" -> + io:format("Mouse: Middle button up, line ~b, column ~b\r~n", [Line, Col]) + end; + ["2", Col0, LineAndButton] -> + {Line, Button} = string:to_integer(LineAndButton), + {Col, ""} = string:to_integer(Col0), + case Button of + "M" -> + io:format("Mouse: Right button down, line ~b, column ~b\r~n", [Line, Col]); + "m" -> + io:format("Mouse: Right button up, line ~b, column ~b\r~n", [Line, Col]) + end; + ["64", Col0, Line0] -> + {Line, _} = string:to_integer(Line0), + {Col, ""} = string:to_integer(Col0), + io:format("Mouse: Wheel up, line ~b, column ~b\r~n", [Line, Col]); + ["65", Col0, Line0] -> + {Line, _} = string:to_integer(Line0), + {Col, ""} = string:to_integer(Col0), + io:format("Mouse: Wheel down, line ~b, column ~b\r~n", [Line, Col]); + _ -> + io:format("Mouse: unparsed mouse event: ~0p\r~n", [Attrs]) + end, + handle_hello_chars(Rest1, Context); +handle_hello_chars([Char | Rest], _Context) -> + io:format("Key: Other: ~p (rest: ~p)\r~n", [Char, Rest]), + stop; +handle_hello_chars("", _Context) -> ok. +split_escape_sequence([Char | Rest], Chars) when Char =:= $m orelse Char =:= $M -> + {lists:reverse([Char | Chars]), Rest}; +split_escape_sequence([$\e | _] = Rest, Chars) -> + {lists:reverse(Chars), Rest}; +split_escape_sequence([Char | Rest], Chars) -> + split_escape_sequence(Rest, [Char | Chars]). + -spec cmd_crash(#rabbit_cli{}) -> no_return(). cmd_crash(_) -> diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index b521af5d8e47..0c708819fcc7 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -274,4 +274,8 @@ terminate(Reason, _Context) -> handle_request({read_file, Filename}) -> file:read_file(Filename); handle_request({write_file, Filename, Bytes}) -> - file:write_file(Filename, Bytes). + file:write_file(Filename, Bytes); +handle_request(set_interactive_mode) -> + Ret = shell:start_interactive({noshell, raw}), + ?LOG_DEBUG("CLI: interactive mode: ~p", [Ret]), + Ret. diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index bc8c76082f2b..58c2aa692b6d 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -7,6 +7,10 @@ -include("src/rabbit_cli_backend.hrl"). -export([argparse_def/1, + read_stdin/1, + read_file/2, + write_file/3, + set_interactive_mode/1, supports_colors/1]). -export([start_link/1, stop/1, @@ -15,8 +19,7 @@ start_record_stream/4, push_new_record/3, end_record_stream/2, - send_keyboard_input/3, - read_file/2]). + send_keyboard_input/3]). -export([init/1, handle_call/3, handle_cast/2, @@ -69,6 +72,33 @@ argparse_def(file_output) -> ] }. +read_stdin(Context) -> + read_stdin(Context, []). + +read_stdin(Context, Data) -> + case io:get_chars("", 4096) of + Chunk when is_list(Chunk) -> + Data1 = [Data, Chunk], + read_stdin(Context, Data1); + eof -> + {ok, list_to_binary(Data)}; + {error, _} = Error -> + Error + end. + +read_file(Context, Filename) -> + Request = {?FUNCTION_NAME, Filename}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +write_file(Context, Filename, Bytes) -> + Request = {?FUNCTION_NAME, Filename, Bytes}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +set_interactive_mode(Context) -> + ?LOG_DEBUG("CLI: request interactive mode"), + Request = ?FUNCTION_NAME, + rabbit_cli_backend:send_frontend_request(Context, Request). + supports_colors(#rabbit_cli{env = Env, terminal = #{info := TermInfo}}) -> ?LOG_DEBUG("Env: ~p", [Env]), case proplists:get_value("COLORTERM", Env) of @@ -137,13 +167,13 @@ send_keyboard_input(IO, ArgMap, Subscriber) is_map(ArgMap) -> gen_server:call(IO, {?FUNCTION_NAME, ArgMap, Subscriber}). -read_file({transport, Transport}, ArgMap) -> - Transport ! {io_call, self(), {?FUNCTION_NAME, ArgMap}}, - receive Ret -> Ret end; -read_file(IO, ArgMap) - when is_pid(IO) andalso - is_map(ArgMap) -> - gen_server:call(IO, {?FUNCTION_NAME, ArgMap}). +% read_file({transport, Transport}, ArgMap) -> +% Transport ! {io_call, self(), {?FUNCTION_NAME, ArgMap}}, +% receive Ret -> Ret end; +% read_file(IO, ArgMap) +% when is_pid(IO) andalso +% is_map(ArgMap) -> +% gen_server:call(IO, {?FUNCTION_NAME, ArgMap}). init(#{progname := Progname}) -> process_flag(trap_exit, true), @@ -169,9 +199,9 @@ handle_call( Subscribers1 = [Subscriber | Subscribers], State1 = State#?MODULE{kbd_subscribers = Subscribers1}, {reply, ok, State1, compute_timeout(State1)}; -handle_call({read_file, ArgMap}, From, State) -> - {ok, State1} = do_read_file(ArgMap, From, State), - {noreply, State1, compute_timeout(State1)}; +% handle_call({read_file, ArgMap}, From, State) -> +% {ok, State1} = do_read_file(ArgMap, From, State), +% {noreply, State1, compute_timeout(State1)}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> @@ -357,22 +387,22 @@ isatty(IoDevice) -> false end. -do_read_file(#{input := "-"}, From, State) -> - Ret = read_stdin(<<>>), - gen:reply(From, Ret), - {ok, State}; -do_read_file(#{input := Filename}, From, State) -> - Ret = file:read_file(Filename), - gen:reply(From, Ret), - {ok, State}. - -read_stdin(Buf) -> - case file:read(standard_io, 4096) of - {ok, Data} -> - Buf1 = [Buf, Data], - read_stdin(Buf1); - eof -> - {ok, Buf}; - {error, _} = Error -> - Error - end. +% do_read_file(#{input := "-"}, From, State) -> +% Ret = read_stdin(<<>>), +% gen:reply(From, Ret), +% {ok, State}; +% do_read_file(#{input := Filename}, From, State) -> +% Ret = file:read_file(Filename), +% gen:reply(From, Ret), +% {ok, State}. +% +% read_stdin(Buf) -> +% case file:read(standard_io, 4096) of +% {ok, Data} -> +% Buf1 = [Buf, Data], +% read_stdin(Buf1); +% eof -> +% {ok, Buf}; +% {error, _} = Error -> +% Error +% end. From 599c88085a4062786d2b1fde497c469952a9a1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 26 Jun 2025 17:17:26 +0200 Subject: [PATCH 41/51] Support paging --- deps/rabbit/src/rabbit_cli_backend.erl | 8 +-- deps/rabbit/src/rabbit_cli_commands.erl | 15 ++++ deps/rabbit/src/rabbit_cli_datagrid.erl | 3 +- deps/rabbit/src/rabbit_cli_frontend.erl | 84 ++++++++++++++++++---- deps/rabbit/src/rabbit_cli_http_client.erl | 4 +- deps/rabbit/src/rabbit_cli_io.erl | 6 ++ deps/rabbit/src/rabbit_exchange.erl | 12 +++- 7 files changed, 109 insertions(+), 23 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index c5aaeca5cf8a..442bf23ef30c 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -20,10 +20,6 @@ -record(?MODULE, {caller}). -%% TODO: -%% * Implémenter "list exchanges" plus proprement -%% * Implémenter "rabbitmqctl list_exchanges" pour la compatibilité - run_command(ContextMap, Caller) when is_map(ContextMap) -> Context = map_to_context(ContextMap), run_command(Context, Caller); @@ -87,11 +83,11 @@ init( args = Args, terminal = Terminal} = Context, caller := Caller, - group_leader := GroupLeader + group_leader := _GroupLeader }) -> process_flag(trap_exit, true), erlang:link(Caller), - erlang:group_leader(GroupLeader, self()), + erlang:group_leader(Caller, self()), ?LOG_INFO("CLI: running: ~0p", [[Progname | Args]]), ?LOG_DEBUG( "CLI: tty: stdout=~s stderr=~s stdin=~s", diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 382d94e59696..332b633d7352 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -14,6 +14,7 @@ -export([cmd_generate_completion_script/1, cmd_noop/1, cmd_hello/1, + cmd_pager/1, cmd_crash/1, cmd_top/1]). @@ -27,6 +28,11 @@ #{help => "Say hello!", handler => {?MODULE, cmd_hello}}}). +-rabbitmq_command( + {#{cli => ["page"]}, + #{help => "Test pager", + handler => {?MODULE, cmd_pager}}}). + -rabbitmq_command( {#{cli => ["crash"]}, #{help => "Crash", @@ -458,6 +464,15 @@ split_escape_sequence([$\e | _] = Rest, Chars) -> split_escape_sequence([Char | Rest], Chars) -> split_escape_sequence(Rest, [Char | Chars]). +cmd_pager(Context) -> + ok = rabbit_cli_io:set_paging_mode(Context), + ok = io:format("Line 1~n"), + ok = io:format("Line 2~n"), + ok = io:format("(waiting)~n"), + timer:sleep(5000), + ok = io:format("Line 3 (last)~n"), + ok. + -spec cmd_crash(#rabbit_cli{}) -> no_return(). cmd_crash(_) -> diff --git a/deps/rabbit/src/rabbit_cli_datagrid.erl b/deps/rabbit/src/rabbit_cli_datagrid.erl index 70f61ab703d4..1dc326c731e7 100644 --- a/deps/rabbit/src/rabbit_cli_datagrid.erl +++ b/deps/rabbit/src/rabbit_cli_datagrid.erl @@ -62,7 +62,8 @@ process_fields(#?MODULE{fields_fun = FieldsFun, priv = Priv} = State) -> end. start_stream( - #?MODULE{setup_stream_fun = SetupStreamFun, priv = Priv} = State) -> + #?MODULE{setup_stream_fun = SetupStreamFun, + priv = Priv} = State) -> maybe {ok, Priv1} ?= SetupStreamFun(Priv), State1 = State#?MODULE{priv = Priv1}, diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 0c708819fcc7..2f3a60fcf396 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -9,7 +9,8 @@ noop/1]). -record(?MODULE, {scriptname, - connection}). + connection, + pager}). -spec main(Args) -> no_return() when Args :: argparse:args(). @@ -253,29 +254,86 @@ record_to_map([], _Record, _Index, Map) -> Map. main_loop( - #rabbit_cli{priv = #?MODULE{connection = Connection}} = Context) -> - ?LOG_DEBUG("CLI: frontend main loop..."), + #rabbit_cli{priv = #?MODULE{connection = Connection, + pager = Pager} = Priv} = Context) -> + ?LOG_DEBUG("CLI: frontend main loop (pager: ~0p)...", [Pager]), + Timeout = case is_port(Pager) of + false -> + infinity; + true -> + 100 + end, receive - {'EXIT', _LinkedPid, Reason} -> - terminate(Reason, Context); + {'EXIT', Pager, Reason} -> + ?LOG_DEBUG("CLI: EXIT signal from pager: ~p", [Reason]), + Priv1 = Priv#?MODULE{pager = undefined}, + Context1 = Context#rabbit_cli{priv = Priv1}, + terminate(Reason, Context1); + {'EXIT', LinkedPid, Reason} -> + ?LOG_DEBUG( + "CLI: EXIT signal from linked process ~0p: ~p", + [LinkedPid, Reason]), + case Pager of + undefined -> + terminate(Reason, Context); + _ -> + ?LOG_DEBUG("CLI: waiting for pager to exit"), + main_loop(Context) + end; {frontend_request, From, Request} -> - Reply = handle_request(Request), + {reply, Reply, Context1} = handle_request(Request, Context), _ = rabbit_cli_transport2:gen_reply(Connection, From, Reply), + main_loop(Context1); + {io_request, From, ReplyAs, Request} + when element(1, Request) =:= put_chars andalso is_port(Pager) -> + Chars0 = case Request of + {put_chars, unicode, M, F, A} -> + erlang:apply(M, F, A); + {put_chars, unicode, C} -> + C + end, + Chars1 = re:replace(Chars0, "\n", "\r\n"), + Bin = unicode:characters_to_binary(Chars1), + erlang:port_command(Pager, Bin), + IoReply = {io_reply, ReplyAs, ok}, + From ! IoReply, + main_loop(Context); + {io_request, _From, _ReplyAs, _Request} = IoRequest -> + GroupLeader = erlang:group_leader(), + GroupLeader ! IoRequest, main_loop(Context); Info -> - ?LOG_DEBUG("Unknown info: ~0p", [Info]), + ?LOG_ALERT("CLI: unknown info: ~0p", [Info]), main_loop(Context) + after Timeout -> + erlang:port_command(Pager, <<>>), + main_loop(Context) end. terminate(Reason, _Context) -> ?LOG_DEBUG("CLI: frontend terminating: ~0p", [Reason]), ok. -handle_request({read_file, Filename}) -> - file:read_file(Filename); -handle_request({write_file, Filename, Bytes}) -> - file:write_file(Filename, Bytes); -handle_request(set_interactive_mode) -> +handle_request({read_file, Filename}, Context) -> + {reply, file:read_file(Filename), Context}; +handle_request({write_file, Filename, Bytes}, Context) -> + {reply, file:write_file(Filename, Bytes), Context}; +handle_request(set_interactive_mode, Context) -> Ret = shell:start_interactive({noshell, raw}), ?LOG_DEBUG("CLI: interactive mode: ~p", [Ret]), - Ret. + {reply, Ret, Context}; +handle_request( + set_paging_mode, #rabbit_cli{env = Env, priv = Priv} = Context) -> + Cmd = case proplists:get_value("PAGER", Env) of + Value when is_list(Value) -> + Value; + undefined -> + "less" + end, + ?LOG_DEBUG("CLI: start pager \"~ts\"", [Cmd]), + Pager = erlang:open_port( + {spawn, Cmd}, + [stream, exit_status, binary, use_stdio, out, hide, {env, Env}]), + Priv1 = Priv#?MODULE{pager = Pager}, + Context1 = Context#rabbit_cli{priv = Priv1}, + {reply, ok, Context1}. diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index d20a795cc118..c4799d39ad90 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -170,11 +170,11 @@ handle_request( {noreply, Data}; handle_request( {io_request, From, ReplyAs, Request}, - #?MODULE{group_leader = GroupLeader, + #?MODULE{caller = Caller, io_requests = IoRequests} = Data) -> ProxyRef = erlang:make_ref(), ProxyIoRequest = {io_request, self(), ProxyRef, Request}, - GroupLeader ! ProxyIoRequest, + Caller ! ProxyIoRequest, IoRequests1 = IoRequests#{ProxyRef => {From, ReplyAs}}, Data1 = Data#?MODULE{io_requests = IoRequests1}, {noreply, Data1}; diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 58c2aa692b6d..75897499bf1b 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -11,6 +11,7 @@ read_file/2, write_file/3, set_interactive_mode/1, + set_paging_mode/1, supports_colors/1]). -export([start_link/1, stop/1, @@ -99,6 +100,11 @@ set_interactive_mode(Context) -> Request = ?FUNCTION_NAME, rabbit_cli_backend:send_frontend_request(Context, Request). +set_paging_mode(Context) -> + ?LOG_DEBUG("CLI: request paging mode"), + Request = ?FUNCTION_NAME, + rabbit_cli_backend:send_frontend_request(Context, Request). + supports_colors(#rabbit_cli{env = Env, terminal = #{info := TermInfo}}) -> ?LOG_DEBUG("Env: ~p", [Env]), case proplists:get_value("COLORTERM", Env) of diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index eaa27b308cf2..a109cd0ff352 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -601,7 +601,10 @@ type_to_route_fun(T) -> %% CLI commands. %% ------------------------------------------------------------------- -cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap, legacy = Legacy} = Context) -> +cmd_list_exchanges( + #rabbit_cli{arg_map = ArgMap, + terminal = Terminal, + legacy = Legacy} = Context) -> VHost = <<"/">>, %% TODO: Take from args and use it. InfoKeys = rabbit_exchange:info_keys(), Fields0 = case ArgMap of @@ -618,6 +621,13 @@ cmd_list_exchanges(#rabbit_cli{arg_map = ArgMap, legacy = Legacy} = Context) -> case Legacy of false -> + case Terminal of + #{stdout := true} -> + ok = rabbit_cli_io:set_paging_mode(Context); + _ -> + ok + end, + case rabbit_cli_io:supports_colors(Context) of {true, truecolor} -> io:format( From 3f960f1747a42f21ff3c84c356530a630ebb88df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 11:00:57 +0200 Subject: [PATCH 42/51] Support Unix signal handling --- deps/rabbit/src/rabbit_cli_commands.erl | 49 ++++++++-- deps/rabbit/src/rabbit_cli_frontend.erl | 101 +++++++++++++++++++-- deps/rabbit/src/rabbit_cli_http_client.erl | 6 +- deps/rabbit/src/rabbit_cli_io.erl | 14 +++ deps/rabbit/src/rabbit_cli_transport2.erl | 8 +- 5 files changed, 159 insertions(+), 19 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index 332b633d7352..fc9025774d93 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -15,6 +15,7 @@ cmd_noop/1, cmd_hello/1, cmd_pager/1, + cmd_signal/1, cmd_crash/1, cmd_top/1]). @@ -33,6 +34,11 @@ #{help => "Test pager", handler => {?MODULE, cmd_pager}}}). +-rabbitmq_command( + {#{cli => ["signal"]}, + #{help => "Test signal handling", + handler => {?MODULE, cmd_signal}}}). + -rabbitmq_command( {#{cli => ["crash"]}, #{help => "Crash", @@ -365,16 +371,21 @@ cmd_noop(_) -> cmd_hello(Context) -> io:format("Regural prompt test; type Enter to submit~n"), Name = io:get_line("Name: "), - io:format("Hello ~s!~n", [string:trim(Name)]), - - io:format( - "~nInteraction mode test; " - "type any arrow keys, click with your mouse, or any other key to quit~n"), - ok = rabbit_cli_io:set_interactive_mode(Context), - io:format("\e[?1003h\e[?1015h\e[?1006h"), - Ret = cmd_hello_loop(Context), - io:format("\e[?1000l"), - Ret. + case Name of + Name when is_list(Name) -> + io:format("Hello ~s!~n", [string:trim(Name)]), + + io:format( + "~nInteraction mode test; " + "type any arrow keys, click with your mouse, or any other key to quit~n"), + ok = rabbit_cli_io:set_interactive_mode(Context), + io:format("\e[?1003h\e[?1015h\e[?1006h"), + Ret = cmd_hello_loop(Context), + io:format("\e[?1000l"), + Ret; + {error, _} = Error -> + Error + end. cmd_hello_loop(Context) -> case io:get_chars("", 1024) of @@ -473,6 +484,24 @@ cmd_pager(Context) -> ok = io:format("Line 3 (last)~n"), ok. +cmd_signal(Context) -> + receive + {signal, sigwinch} -> + #{lines := Lines, cols := Cols} = rabbit_cli_io:get_window_size( + Context), + io:format("Window size: ~bx~b~n", [Cols, Lines]), + cmd_signal(Context); + {signal, siginfo} -> + ?LOG_DEBUG("CLI: siginfo"), + {current_stacktrace, Stacktrace} = erlang:process_info( + self(), current_stacktrace), + rabbit_cli_io:display_siginfo( + Context, + "RabbitMQ CLI in:~n ~p~n", + [Stacktrace]), + cmd_signal(Context) + end. + -spec cmd_crash(#rabbit_cli{}) -> no_return(). cmd_crash(_) -> diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 2f3a60fcf396..6e74b2b85b2f 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -1,5 +1,7 @@ -module(rabbit_cli_frontend). +-behaviour(gen_event). + -include_lib("kernel/include/logger.hrl"). -include_lib("stdlib/include/assert.hrl"). @@ -7,9 +9,15 @@ -export([main/1, noop/1]). +-export([init/1, + handle_call/2, + handle_event/2, + terminate/2, + code_change/3]). -record(?MODULE, {scriptname, connection, + backend, pager}). -spec main(Args) -> no_return() when @@ -67,6 +75,7 @@ run_cli(ScriptName, Args) -> ProgName0 = filename:basename(ScriptName, ".bat"), ProgName1 = filename:basename(ProgName0, ".escript"), Terminal = collect_terminal_info(), + configure_signal_handler(), Priv = #?MODULE{scriptname = ScriptName}, Context = #rabbit_cli{progname = ProgName1, args = Args, @@ -97,6 +106,20 @@ collect_terminal_info() -> name => Term, info => TermInfo}. +configure_signal_handler() -> + gen_event:add_handler(erl_signal_server, ?MODULE, self()), + Signals = [sigwinch, siginfo], + lists:foreach( + fun(Signal) -> + try + os:set_signal(Signal, handle) + catch + _:badarg -> + ?LOG_DEBUG("Signal ~s not supported", [Signal]) + end + end, Signals), + ok. + init_local_args(Context) -> maybe LocalArgparseDef = initial_argparse_def(), @@ -217,14 +240,16 @@ noop(_Context) -> %% * evolutions in the communication between the frontend and the backend run_command( - #rabbit_cli{priv = #?MODULE{connection = Connection}} = Context) + #rabbit_cli{priv = #?MODULE{connection = Connection} = Priv} = Context) when Connection =/= none -> maybe process_flag(trap_exit, true), ContextMap = context_to_map(Context), - {ok, _Backend} ?= rabbit_cli_transport2:run_command( + {ok, Backend} ?= rabbit_cli_transport2:run_command( Connection, ContextMap), - main_loop(Context) + Priv1 = Priv#?MODULE{backend = Backend}, + Context1 = Context#rabbit_cli{priv = Priv1}, + main_loop(Context1) end; run_command(#rabbit_cli{} = Context) -> %% TODO: If we can't connect to a node, try to parse args locally and run @@ -255,6 +280,7 @@ record_to_map([], _Record, _Index, Map) -> main_loop( #rabbit_cli{priv = #?MODULE{connection = Connection, + backend = Backend, pager = Pager} = Priv} = Context) -> ?LOG_DEBUG("CLI: frontend main loop (pager: ~0p)...", [Pager]), Timeout = case is_port(Pager) of @@ -268,14 +294,14 @@ main_loop( ?LOG_DEBUG("CLI: EXIT signal from pager: ~p", [Reason]), Priv1 = Priv#?MODULE{pager = undefined}, Context1 = Context#rabbit_cli{priv = Priv1}, - terminate(Reason, Context1); + terminate_cli(Reason, Context1); {'EXIT', LinkedPid, Reason} -> ?LOG_DEBUG( "CLI: EXIT signal from linked process ~0p: ~p", [LinkedPid, Reason]), case Pager of undefined -> - terminate(Reason, Context); + terminate_cli(Reason, Context); _ -> ?LOG_DEBUG("CLI: waiting for pager to exit"), main_loop(Context) @@ -302,6 +328,10 @@ main_loop( GroupLeader = erlang:group_leader(), GroupLeader ! IoRequest, main_loop(Context); + {signal, Signal} = Event -> + ?LOG_DEBUG("CLI: got Unix signal: ~ts", [Signal]), + _ = rabbit_cli_transport2:send(Connection, Backend, Event), + main_loop(Context); Info -> ?LOG_ALERT("CLI: unknown info: ~0p", [Info]), main_loop(Context) @@ -310,7 +340,7 @@ main_loop( main_loop(Context) end. -terminate(Reason, _Context) -> +terminate_cli(Reason, _Context) -> ?LOG_DEBUG("CLI: frontend terminating: ~0p", [Reason]), ok. @@ -336,4 +366,61 @@ handle_request( [stream, exit_status, binary, use_stdio, out, hide, {env, Env}]), Priv1 = Priv#?MODULE{pager = Pager}, Context1 = Context#rabbit_cli{priv = Priv1}, - {reply, ok, Context1}. + {reply, ok, Context1}; +handle_request({display_siginfo, {Format, Args}}, Context) -> + io:format(standard_error, Format, Args), + {reply, ok, Context}; +handle_request({display_siginfo, String}, Context) -> + io:format(standard_error, "~ts", [String]), + {reply, ok, Context}; +handle_request(get_window_size, Context) -> + Size = get_window_size(Context), + {reply, Size, Context}. + +get_window_size(#rabbit_cli{env = Env}) -> + DefaultSize = #{lines => 25, cols => 80}, + Cmd = "stty size", + Port = erlang:open_port( + {spawn, Cmd}, + [stream, use_stdio, in, hide, {env, Env}]), + get_window_size_loop(Port, DefaultSize). + +get_window_size_loop(Port, Size) -> + receive + {Port, {data, Output}} -> + [LinesStr, ColsStr] = string:lexemes(string:trim(Output), " "), + Lines = list_to_integer(LinesStr), + Cols = list_to_integer(ColsStr), + Size1 = Size#{lines => Lines, + cols => Cols}, + get_window_size_loop(Port, Size1); + {'EXIT', Port, _Reason} -> + Size + end. + +%% ------------------------------------------------------------------- +%% gen_event callbacks (signal handler). +%% ------------------------------------------------------------------- + +init(Parent) -> + {ok, Parent}. + +handle_call(Request, Parent) -> + ?LOG_DEBUG("CLI: (signal) unknown request: ~0p", [Request]), + {ok, ok, Parent}. + +handle_event(Signal, Parent) + when Signal =:= sigwinch orelse Signal =:= siginfo -> + ?LOG_DEBUG("CLI: (signal) signal = ~0p", [Signal]), + Parent ! {signal, Signal}, + {ok, Parent}; +handle_event(Event, Parent) -> + ?LOG_DEBUG("CLI: (signal) unknown event: ~0p", [Event]), + {ok, Parent}. + +terminate(Reason, _Parent) -> + ?LOG_DEBUG("CLI: (signal) terminate: ~0p", [Reason]), + ok. + +code_change(_OldVsn, Parent, _Extra) -> + {ok, Parent}. diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index c4799d39ad90..4803b53c2f75 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -6,7 +6,8 @@ -export([start_link/1, run_command/2, - gen_reply/3]). + gen_reply/3, + send/3]). -export([init/1, callback_mode/0, handle_event/4, @@ -31,6 +32,9 @@ run_command(Client, ContextMap) -> gen_reply(Client, From, Reply) -> gen_statem:cast(Client, {?FUNCTION_NAME, From, Reply}). +send(Client, Dest, Msg) -> + gen_statem:cast(Client, {?FUNCTION_NAME, Dest, Msg}). + %% ------------------------------------------------------------------- %% gen_statem callbacks. %% ------------------------------------------------------------------- diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl index 75897499bf1b..4de1906789bb 100644 --- a/deps/rabbit/src/rabbit_cli_io.erl +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -12,6 +12,8 @@ write_file/3, set_interactive_mode/1, set_paging_mode/1, + display_siginfo/2, display_siginfo/3, + get_window_size/1, supports_colors/1]). -export([start_link/1, stop/1, @@ -105,6 +107,18 @@ set_paging_mode(Context) -> Request = ?FUNCTION_NAME, rabbit_cli_backend:send_frontend_request(Context, Request). +display_siginfo(Context, String) -> + Request = {?FUNCTION_NAME, String}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +display_siginfo(Context, Format, Args) -> + Request = {?FUNCTION_NAME, {Format, Args}}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +get_window_size(Context) -> + Request = ?FUNCTION_NAME, + rabbit_cli_backend:send_frontend_request(Context, Request). + supports_colors(#rabbit_cli{env = Env, terminal = #{info := TermInfo}}) -> ?LOG_DEBUG("Env: ~p", [Env]), case proplists:get_value("COLORTERM", Env) of diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index fca643d91791..0243287c22c0 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -4,7 +4,8 @@ -export([connect/0, connect/1, run_command/2, - gen_reply/3]). + gen_reply/3, + send/3]). -record(?MODULE, {type :: erldist | http, peer :: atom() | pid()}). @@ -95,3 +96,8 @@ gen_reply(#?MODULE{type = erldist}, From, Reply) -> gen:reply(From, Reply); gen_reply(#?MODULE{type = http, peer = Client}, From, Reply) -> rabbit_cli_http_client:gen_reply(Client, From, Reply). + +send(#?MODULE{type = erldist}, Dest, Msg) -> + erlang:send(Dest, Msg); +send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> + rabbit_cli_http_client:send(Client, Dest, Msg). From 50b18ae92527a109bde9e47ca2298041aba11dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 14:09:01 +0200 Subject: [PATCH 43/51] Add ability to list currently running commands --- deps/rabbit/src/rabbit_cli_backend.erl | 65 +++++++++++++++++++++- deps/rabbit/src/rabbit_cli_backend.hrl | 2 + deps/rabbit/src/rabbit_cli_backend_sup.erl | 7 ++- deps/rabbit/src/rabbit_cli_frontend.erl | 18 +++--- deps/rabbit/src/rabbit_cli_transport2.erl | 6 ++ 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 442bf23ef30c..fd5239b8fbf6 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -11,7 +11,8 @@ -export([run_command/2, send_frontend_request/2, - start_link/3]). + start_link/3, + i/0]). -export([init/1, callback_mode/0, handle_event/4, @@ -38,6 +39,7 @@ map_to_context(ContextMap) -> command = maps:get(command, ContextMap), legacy = Legacy, os = maps:get(os, ContextMap), + client = maps:get(client, ContextMap), env = maps:get(env, ContextMap), terminal = maps:get(terminal, ContextMap)}. @@ -74,6 +76,37 @@ send_frontend_request( exit(Reason) end. +i() -> + Backends = rabbit_cli_backend_sup:which_backends(), + i(Backends). + +i([]) -> + io:format("No CLI commands running~n"); +i(Backends) -> + io:format("Running commands:~n"), + Now = erlang:system_time(), + lists:foreach( + fun(Backend) -> + #{cmdline := CmdLine, + client := #{hostname := Hostname, proto := Proto}, + started_at := StartTime} = proc_lib:get_label(Backend), + Duration = erlang:convert_time_unit( + Now - StartTime, native, seconds), + FormattedCmdLine = format_cmdline(CmdLine), + FormattedProto = case Proto of + erldist -> + "Erlang distribution"; + http -> + "HTTP"; + _ -> + Proto + end, + io:format( + " - ~ts~n (running for ~b seconds, started from \"~ts\", " + "protocol: ~ts)~n", + [FormattedCmdLine, Duration, Hostname, FormattedProto]) + end, Backends). + %% ------------------------------------------------------------------- %% gen_statem callbacks. %% ------------------------------------------------------------------- @@ -81,14 +114,28 @@ send_frontend_request( init( #{context := #rabbit_cli{progname = Progname, args = Args, + client = #{hostname := Hostname, + proto := Proto} = ClientInfo, terminal = Terminal} = Context, caller := Caller, group_leader := _GroupLeader }) -> - process_flag(trap_exit, true), + %% Do not trap EXIT signal. This ensures the command is stopped. Because + %% it could be running a blocking call or receive and the EXIT signal + %% could seat in its inbox for a long time. + erlang:link(Caller), erlang:group_leader(Caller, self()), - ?LOG_INFO("CLI: running: ~0p", [[Progname | Args]]), + + CmdLine = [Progname | Args], + StartTime = erlang:system_time(), + Label = #{cmdline => CmdLine, + client => ClientInfo, + started_at => StartTime}, + proc_lib:set_label(Label), + ?LOG_INFO( + "CLI: running: ~ts (started from \"~ts\", protocol: ~ts)", + [format_cmdline(CmdLine), Hostname, Proto]), ?LOG_DEBUG( "CLI: tty: stdout=~s stderr=~s stdin=~s", [maps:get(stdout, Terminal), @@ -99,6 +146,18 @@ init( Context1 = Context#rabbit_cli{priv = Priv}, {ok, standing_by, Context1, {next_event, internal, parse_command}}. +format_cmdline(CmdLine) -> + FormattedArgs = [begin + case string:chr(Arg, $') of + 0 -> + Arg; + _ -> + Arg1 = re:replace(Arg, "'", "\\'", [global]), + "'" ++ Arg1 ++ "'" + end + end || Arg <- CmdLine], + string:join(FormattedArgs, " "). + callback_mode() -> handle_event_function. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl index 7dab373a56a7..975b9ccc225d 100644 --- a/deps/rabbit/src/rabbit_cli_backend.hrl +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -7,6 +7,8 @@ legacy :: boolean() | undefined, os :: {unix | win32, atom()}, + client :: #{hostname := string(), + proto := atom()} | undefined, env :: [{os:env_var_name(), os:env_var_value()}], terminal :: #{stdout := boolean(), stderr := boolean(), diff --git a/deps/rabbit/src/rabbit_cli_backend_sup.erl b/deps/rabbit/src/rabbit_cli_backend_sup.erl index e8c73b067663..cb843fc12104 100644 --- a/deps/rabbit/src/rabbit_cli_backend_sup.erl +++ b/deps/rabbit/src/rabbit_cli_backend_sup.erl @@ -3,7 +3,8 @@ -behaviour(supervisor). -export([start_link/0, - start_backend/3]). + start_backend/3, + which_backends/0]). -export([init/1]). start_link() -> @@ -12,6 +13,10 @@ start_link() -> start_backend(Context, Caller, GroupLeader) -> supervisor:start_child(?MODULE, [Context, Caller, GroupLeader]). +which_backends() -> + Children = supervisor:which_children(?MODULE), + [Child || {_ChildId, Child, _Type, _Modules} <- Children]. + init(_Args) -> SupFlags = #{strategy => simple_one_for_one}, BackendChild = #{id => rabbit_cli_backend, diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 6e74b2b85b2f..d59492acd9e6 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -150,13 +150,17 @@ connect_to_node( _ -> rabbit_cli_transport2:connect() end, - Priv1 = case Ret of - {ok, Connection} -> - Priv#?MODULE{connection = Connection}; - {error, _Reason} -> - Priv#?MODULE{connection = none} - end, - Context1 = Context#rabbit_cli{priv = Priv1}, + {ClientInfo, Priv1} = case Ret of + {ok, Connection} -> + {rabbit_cli_transport2:get_client_info( + Connection), + Priv#?MODULE{connection = Connection}}; + {error, _Reason} -> + {undefined, + Priv#?MODULE{connection = none}} + end, + Context1 = Context#rabbit_cli{client = ClientInfo, + priv = Priv1}, run_command(Context1). %% ------------------------------------------------------------------- diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl index 0243287c22c0..f85ff37b7242 100644 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ b/deps/rabbit/src/rabbit_cli_transport2.erl @@ -3,6 +3,7 @@ -include_lib("kernel/include/logger.hrl"). -export([connect/0, connect/1, + get_client_info/1, run_command/2, gen_reply/3, send/3]). @@ -86,6 +87,11 @@ complete_nodename(Nodename) -> list_to_atom(Nodename) end. +get_client_info(#?MODULE{type = Proto}) -> + {ok, Hostname} = inet:gethostname(), + #{hostname => Hostname, + proto => Proto}. + run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> Caller = self(), erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); From 1e1d16583a96cb2e6efeaf8cecbb859d8c07668f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 14:21:19 +0200 Subject: [PATCH 44/51] Remove passing of group leader --- deps/rabbit/src/rabbit_cli_backend.erl | 13 +++++-------- deps/rabbit/src/rabbit_cli_backend_sup.erl | 6 +++--- deps/rabbit/src/rabbit_cli_http_client.erl | 5 +---- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index fd5239b8fbf6..412583cc7027 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -11,7 +11,7 @@ -export([run_command/2, send_frontend_request/2, - start_link/3, + start_link/2, i/0]). -export([init/1, callback_mode/0, @@ -25,8 +25,7 @@ run_command(ContextMap, Caller) when is_map(ContextMap) -> Context = map_to_context(ContextMap), run_command(Context, Caller); run_command(#rabbit_cli{} = Context, Caller) when is_pid(Caller) -> - GroupLeader = erlang:group_leader(), - rabbit_cli_backend_sup:start_backend(Context, Caller, GroupLeader). + rabbit_cli_backend_sup:start_backend(Context, Caller). map_to_context(ContextMap) -> Progname = maps:get(progname, ContextMap), @@ -58,10 +57,9 @@ is_legacy_progname("rabbitmq-upgrade") -> is_legacy_progname(_Progname) -> false. -start_link(Context, Caller, GroupLeader) -> +start_link(Context, Caller) -> Args = #{context => Context, - caller => Caller, - group_leader => GroupLeader}, + caller => Caller}, gen_statem:start_link(?MODULE, Args, []). send_frontend_request( @@ -117,8 +115,7 @@ init( client = #{hostname := Hostname, proto := Proto} = ClientInfo, terminal = Terminal} = Context, - caller := Caller, - group_leader := _GroupLeader + caller := Caller }) -> %% Do not trap EXIT signal. This ensures the command is stopped. Because %% it could be running a blocking call or receive and the EXIT signal diff --git a/deps/rabbit/src/rabbit_cli_backend_sup.erl b/deps/rabbit/src/rabbit_cli_backend_sup.erl index cb843fc12104..dbb9a61aed1a 100644 --- a/deps/rabbit/src/rabbit_cli_backend_sup.erl +++ b/deps/rabbit/src/rabbit_cli_backend_sup.erl @@ -3,15 +3,15 @@ -behaviour(supervisor). -export([start_link/0, - start_backend/3, + start_backend/2, which_backends/0]). -export([init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, none). -start_backend(Context, Caller, GroupLeader) -> - supervisor:start_child(?MODULE, [Context, Caller, GroupLeader]). +start_backend(Context, Caller) -> + supervisor:start_child(?MODULE, [Context, Caller]). which_backends() -> Children = supervisor:which_children(?MODULE), diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl index 4803b53c2f75..e24337d64c22 100644 --- a/deps/rabbit/src/rabbit_cli_http_client.erl +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -19,8 +19,7 @@ stream :: gun:stream_ref() | undefined, delayed_requests = [] :: list(), io_requests = #{} :: map(), - caller :: pid(), - group_leader :: pid()}). + caller :: pid()}). start_link(Uri) -> Caller = self(), @@ -42,7 +41,6 @@ send(Client, Dest, Msg) -> init(#{uri := Uri, caller := Caller}) -> maybe #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), - GroupLeader = erlang:group_leader(), {ok, _} ?= application:ensure_all_started(gun), @@ -51,7 +49,6 @@ init(#{uri := Uri, caller := Caller}) -> Data = #?MODULE{uri = UriMap, caller = Caller, - group_leader = GroupLeader, connection = ConnPid}, {ok, opening_connection, Data} end. From 54be38af43fecb09f75dea56b50c360fe69719e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 14:50:38 +0200 Subject: [PATCH 45/51] Fix linking between HTTP client and backend --- deps/rabbit/src/rabbit_cli_http_listener.erl | 50 +++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl index 9cf919107a4e..f1cf076b76e1 100644 --- a/deps/rabbit/src/rabbit_cli_http_listener.erl +++ b/deps/rabbit/src/rabbit_cli_http_listener.erl @@ -135,13 +135,13 @@ websocket_handle({binary, RequestBin}, State) -> Request = binary_to_term(RequestBin), ?LOG_DEBUG("CLI: received HTTP message from client: ~p", [Request]), try - case handle_request(Request) of - {reply, Reply} -> + case handle_request(Request, State) of + {reply, Reply, State1} -> ReplyBin = term_to_binary(Reply), Frame1 = {binary, ReplyBin}, - {[Frame1], State}; - noreply -> - {ok, State} + {[Frame1], State1}; + {noreply, State1} -> + {ok, State1} end catch Class:Reason:Stacktrace -> @@ -167,8 +167,14 @@ websocket_info({'EXIT', _Pid, _Reason} = Exit, State) -> Frame = {binary, ExitBin}, {[Frame, close], State}. -terminate(Reason, _Req, _State) -> +terminate(Reason, _Req, State) -> ?LOG_DEBUG("CLI: HTTP server terminating: ~0p", [Reason]), + case State of + #{backend := Backend} -> + _ = catch erlang:exit(Backend, Reason); + _ -> + ok + end, ok. %% ------------------------------------------------------------------- @@ -188,18 +194,26 @@ reply_with_help(Req, Code) -> Code, #{<<"content-type">> => <<"text/html; charset=utf-8">>}, Body, Req). -handle_request({call, From, Command}) -> - Ret = handle_command(Command), +handle_request({call, From, Command}, State) -> + {Ret, State1} = handle_command(Command, State), Reply = {call_ret, From, Ret}, - {reply, Reply}; -handle_request({cast, Command}) -> - _ = handle_command(Command), - noreply. + {reply, Reply, State1}; +handle_request({cast, Command}, State) -> + {_, State1} = handle_command(Command, State), + {noreply, State1}. -handle_command({run_command, ContextMap}) -> +handle_command({run_command, ContextMap}, State) -> Caller = self(), - rabbit_cli_backend:run_command(ContextMap, Caller); -handle_command({gen_reply, From, Reply}) -> - gen:reply(From, Reply); -handle_command({send, Dest, Msg}) -> - erlang:send(Dest, Msg). + case rabbit_cli_backend:run_command(ContextMap, Caller) of + {ok, Backend} = Ret -> + State1 = State#{backend => Backend}, + {Ret, State1}; + {error, _} = Error -> + {Error, State} + end; +handle_command({gen_reply, From, Reply}, State) -> + Ret = gen:reply(From, Reply), + {Ret, State}; +handle_command({send, Dest, Msg}, State) -> + Ret = erlang:send(Dest, Msg), + {Ret, State}. From 7a388cdeb2d221f3aaf7ade61794a571a515867e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 14:50:54 +0200 Subject: [PATCH 46/51] Fix undef call --- deps/rabbit/src/rabbit_definitions.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deps/rabbit/src/rabbit_definitions.erl b/deps/rabbit/src/rabbit_definitions.erl index 87449eaa71db..7115d8ba61fc 100644 --- a/deps/rabbit/src/rabbit_definitions.erl +++ b/deps/rabbit/src/rabbit_definitions.erl @@ -1195,7 +1195,7 @@ cmd_import_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> end. import_from_file(Context, Filename) -> - case rabbit_cli_backend:read_file(Context, Filename) of + case rabbit_cli_io:read_file(Context, Filename) of {ok, Data} -> do_import(Context, Data); {error, _} = Error -> @@ -1203,7 +1203,7 @@ import_from_file(Context, Filename) -> end. import_from_stdin(Context) -> - case rabbit_cli_backend:read_stdin(Context) of + case rabbit_cli_io:read_stdin(Context) of {ok, Data} -> do_import(Context, Data); {error, _} = Error -> @@ -1236,7 +1236,7 @@ cmd_export_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> export_to_file(Context, Defs, Filename) -> Json = json:encode(Defs), - rabbit_cli_backend:write_file(Context, Filename, Json). + rabbit_cli_io:write_file(Context, Filename, Json). export_to_stdin(#rabbit_cli{terminal = Terminal}, Defs) -> Json = case Terminal of From a381064918268393a9d75acd23261765797be1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 15:03:10 +0200 Subject: [PATCH 47/51] Remove older experimental code --- deps/rabbit/src/rabbit.erl | 4 +- deps/rabbit/src/rabbit_cli.erl | 242 ------------------- deps/rabbit/src/rabbit_cli_curses.erl | 6 - deps/rabbit/src/rabbit_cli_frontend.erl | 12 +- deps/rabbit/src/rabbit_cli_http_listener.erl | 219 ----------------- deps/rabbit/src/rabbit_cli_http_server.erl | 224 +++++++++++++---- deps/rabbit/src/rabbit_cli_transport.erl | 231 +++++------------- deps/rabbit/src/rabbit_cli_transport2.erl | 109 --------- deps/rabbit/src/rabbit_cli_ws.erl | 114 --------- deps/rabbit/src/rabbit_cli_ws_runner.erl | 62 ----- 10 files changed, 251 insertions(+), 972 deletions(-) delete mode 100644 deps/rabbit/src/rabbit_cli.erl delete mode 100644 deps/rabbit/src/rabbit_cli_curses.erl delete mode 100644 deps/rabbit/src/rabbit_cli_http_listener.erl delete mode 100644 deps/rabbit/src/rabbit_cli_transport2.erl delete mode 100644 deps/rabbit/src/rabbit_cli_ws.erl delete mode 100644 deps/rabbit/src/rabbit_cli_ws_runner.erl diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index 1c968329916f..50df0d421ed4 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -248,10 +248,10 @@ []}}, {requires, [core_initialized, recovery]}, {enables, routing_ready}]}). --rabbit_boot_step({rabbit_cli_http_listener, +-rabbit_boot_step({rabbit_cli_http_server, [{description, "RabbitMQ CLI HTTP listener"}, {mfa, {rabbit_sup, start_restartable_child, - [rabbit_cli_http_listener]}}, + [rabbit_cli_http_server]}}, {requires, [core_initialized, recovery]}, {enables, routing_ready}]}). diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl deleted file mode 100644 index f79e2f43db29..000000000000 --- a/deps/rabbit/src/rabbit_cli.erl +++ /dev/null @@ -1,242 +0,0 @@ --module(rabbit_cli). - --include_lib("kernel/include/logger.hrl"). --include_lib("stdlib/include/assert.hrl"). - --export([main/1, - merge_argparse_def/2, - noop/1]). - -main(Args) -> - Ret = run_cli(Args), - io:format(standard_error, "Ret: ~p~n", [Ret]), - erlang:halt(). - -run_cli(Args) -> - maybe - Progname = escript:script_name(), - ok ?= add_rabbitmq_code_path(Progname), - - do_run_cli(Progname, Args) - end. - -do_run_cli(Progname, Args) -> - PartialArgparseDef = argparse_def(), - Context0 = #{progname => Progname, - args => Args, - group_leader => erlang:group_leader(), - argparse_def => PartialArgparseDef}, - maybe - {ok, - PartialArgMap, - PartialCmdPath, - PartialCommand} ?= initial_parse(Context0), - Context1 = Context0#{arg_map => PartialArgMap, - cmd_path => PartialCmdPath, - command => PartialCommand}, - - {ok, Config} ?= read_config_file(Context1), - Context2 = Context1#{config => Config}, - - Context3 = case rabbit_cli_transport:connect(Context2) of - {ok, Connection} -> - Context2#{connection => Connection}; - {error, _} -> - Context2 - end, - - %% We can query the argparse definition from the remote node to know - %% the commands it supports and proceed with the execution. - {ok, ArgparseDef} ?= get_final_argparse_def(Context3), - Context4 = Context3#{argparse_def => ArgparseDef}, - {ok, - ArgMap, - CmdPath, - Command} ?= final_parse(Context4), - Context5 = Context4#{arg_map => ArgMap, - cmd_path => CmdPath, - command => Command}, - - run_command(Context5) - end. - -%% ------------------------------------------------------------------- -%% RabbitMQ code directory. -%% ------------------------------------------------------------------- - -add_rabbitmq_code_path(Progname) -> - ScriptDir = filename:dirname(Progname), - PluginsDir0 = filename:join([ScriptDir, "..", "plugins"]), - PluginsDir1 = case filelib:is_dir(PluginsDir0) of - true -> - PluginsDir0 - end, - Glob = filename:join([PluginsDir1, "*", "ebin"]), - AppDirs = filelib:wildcard(Glob), - lists:foreach(fun code:add_path/1, AppDirs), - ok. - -%% ------------------------------------------------------------------- -%% Arguments definition and parsing. -%% ------------------------------------------------------------------- - -argparse_def() -> - #{arguments => - [ - #{name => help, - long => "-help", - short => $h, - type => boolean, - help => "Display help and exit"}, - #{name => node, - long => "-node", - short => $n, - type => string, - nargs => 1, - help => "Name of the node to control"}, - #{name => verbose, - long => "-verbose", - short => $v, - action => count, - help => - "Be verbose; can be specified multiple times to increase verbosity"}, - #{name => version, - long => "-version", - short => $V, - type => boolean, - help => - "Display version and exit"} - ], - - handler => {?MODULE, noop}}. - -initial_parse( - #{progname := Progname, args := Args, argparse_def := ArgparseDef}) -> - Options = #{progname => Progname}, - case partial_parse(Args, ArgparseDef, Options) of - {ok, ArgMap, CmdPath, Command, _RemainingArgs} -> - {ok, ArgMap, CmdPath, Command}; - {error, _} = Error-> - Error - end. - -partial_parse(Args, ArgparseDef, Options) -> - partial_parse(Args, ArgparseDef, Options, []). - -partial_parse(Args, ArgparseDef, Options, RemainingArgs) -> - case argparse:parse(Args, ArgparseDef, Options) of - {ok, ArgMap, CmdPath, Command} -> - RemainingArgs1 = lists:reverse(RemainingArgs), - {ok, ArgMap, CmdPath, Command, RemainingArgs1}; - {error, {_CmdPath, undefined, Arg, <<>>}} -> - Args1 = Args -- [Arg], - RemainingArgs1 = [Arg | RemainingArgs], - partial_parse(Args1, ArgparseDef, Options, RemainingArgs1); - {error, _} = Error -> - Error - end. - -get_final_argparse_def(#{argparse_def := PartialArgparseDef} = Context) -> - maybe - {ok, FullArgparseDef} ?= get_full_argparse_def(Context), - ArgparseDef1 = merge_argparse_def(PartialArgparseDef, FullArgparseDef), - {ok, ArgparseDef1} - end. - -get_full_argparse_def(#{connection := Connection}) -> - RemoteArgparseDef = rabbit_cli_transport:rpc( - Connection, rabbit_cli_commands, argparse_def, []), - {ok, RemoteArgparseDef}; -get_full_argparse_def(_) -> - LocalArgparseDef = rabbit_cli_commands:argparse_def(), - {ok, LocalArgparseDef}. - -merge_argparse_def(ArgparseDef1, ArgparseDef2) -> - Args1 = maps:get(arguments, ArgparseDef1, []), - Args2 = maps:get(arguments, ArgparseDef2, []), - Args = merge_arguments(Args1, Args2), - Cmds1 = maps:get(commands, ArgparseDef1, #{}), - Cmds2 = maps:get(commands, ArgparseDef2, #{}), - Cmds = merge_commands(Cmds1, Cmds2), - maps:merge( - ArgparseDef1, - ArgparseDef2#{arguments => Args, commands => Cmds}). - -merge_arguments(Args1, Args2) -> - Args1 ++ Args2. - -merge_commands(Cmds1, Cmds2) -> - maps:merge(Cmds1, Cmds2). - -final_parse( - #{progname := Progname, args := Args, argparse_def := ArgparseDef}) -> - Options = #{progname => Progname}, - argparse:parse(Args, ArgparseDef, Options). - -%% ------------------------------------------------------------------- -%% Configuation file. -%% ------------------------------------------------------------------- - -read_config_file(_Context) -> - ConfigFilename = get_config_filename(), - case filelib:is_regular(ConfigFilename) of - true -> - SchemaFilename = get_config_schema_filename(), - Schema = cuttlefish_schema:files([SchemaFilename]), - case cuttlefish_conf:files([ConfigFilename]) of - {errorlist, Errors} -> - io:format(standard_error, "Errors1 = ~p~n", [Errors]), - {error, config}; - Config0 -> - case cuttlefish_generator:map(Schema, Config0) of - {error, _Phase, {errorlist, Errors}} -> - io:format( - standard_error, "Errors2 = ~p~n", [Errors]), - {error, config}; - Config1 -> - Config2 = proplists:get_value( - rabbitmqctl, Config1, []), - Config3 = maps:from_list(Config2), - {ok, Config3} - end - end; - false -> - {ok, #{}} - end. - -get_config_schema_filename() -> - ok = application:load(rabbit), - RabbitPrivDir = code:priv_dir(rabbit), - RabbitmqctlSchema = filename:join( - [RabbitPrivDir, "schema", "rabbitmqctl.schema"]), - RabbitmqctlSchema. - -get_config_filename() -> - {OsFamily, _} = os:type(), - get_config_filename(OsFamily). - -get_config_filename(unix) -> - XdgConfigHome = case os:getenv("XDG_CONFIG_HOME") of - false -> - HomeDir = os:getenv("HOME"), - ?assertNotEqual(false, HomeDir), - filename:join([HomeDir, ".config"]); - Value -> - Value - end, - ConfigFilename = filename:join( - [XdgConfigHome, "rabbitmq", "rabbitmqctl.conf"]), - ConfigFilename. - -%% ------------------------------------------------------------------- -%% Command execution. -%% ------------------------------------------------------------------- - -run_command(#{connection := Connection} = Context) -> - rabbit_cli_transport:rpc( - Connection, rabbit_cli_commands, run_command, [Context]); -run_command(Context) -> - rabbit_cli_commands:run_command(Context). - -noop(_Context) -> - ok. diff --git a/deps/rabbit/src/rabbit_cli_curses.erl b/deps/rabbit/src/rabbit_cli_curses.erl deleted file mode 100644 index 55161868b35b..000000000000 --- a/deps/rabbit/src/rabbit_cli_curses.erl +++ /dev/null @@ -1,6 +0,0 @@ --module(rabbit_cli_curses). - --export([init/0]). - -init() -> - window. diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index d59492acd9e6..4b1224e004ca 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -146,13 +146,13 @@ connect_to_node( #rabbit_cli{arg_map = ArgMap, priv = Priv} = Context) -> Ret = case ArgMap of #{node := NodenameOrUri} -> - rabbit_cli_transport2:connect(NodenameOrUri); + rabbit_cli_transport:connect(NodenameOrUri); _ -> - rabbit_cli_transport2:connect() + rabbit_cli_transport:connect() end, {ClientInfo, Priv1} = case Ret of {ok, Connection} -> - {rabbit_cli_transport2:get_client_info( + {rabbit_cli_transport:get_client_info( Connection), Priv#?MODULE{connection = Connection}}; {error, _Reason} -> @@ -249,7 +249,7 @@ run_command( maybe process_flag(trap_exit, true), ContextMap = context_to_map(Context), - {ok, Backend} ?= rabbit_cli_transport2:run_command( + {ok, Backend} ?= rabbit_cli_transport:run_command( Connection, ContextMap), Priv1 = Priv#?MODULE{backend = Backend}, Context1 = Context#rabbit_cli{priv = Priv1}, @@ -312,7 +312,7 @@ main_loop( end; {frontend_request, From, Request} -> {reply, Reply, Context1} = handle_request(Request, Context), - _ = rabbit_cli_transport2:gen_reply(Connection, From, Reply), + _ = rabbit_cli_transport:gen_reply(Connection, From, Reply), main_loop(Context1); {io_request, From, ReplyAs, Request} when element(1, Request) =:= put_chars andalso is_port(Pager) -> @@ -334,7 +334,7 @@ main_loop( main_loop(Context); {signal, Signal} = Event -> ?LOG_DEBUG("CLI: got Unix signal: ~ts", [Signal]), - _ = rabbit_cli_transport2:send(Connection, Backend, Event), + _ = rabbit_cli_transport:send(Connection, Backend, Event), main_loop(Context); Info -> ?LOG_ALERT("CLI: unknown info: ~0p", [Info]), diff --git a/deps/rabbit/src/rabbit_cli_http_listener.erl b/deps/rabbit/src/rabbit_cli_http_listener.erl deleted file mode 100644 index f1cf076b76e1..000000000000 --- a/deps/rabbit/src/rabbit_cli_http_listener.erl +++ /dev/null @@ -1,219 +0,0 @@ --module(rabbit_cli_http_listener). - --behaviour(gen_server). --behaviour(cowboy_websocket). - --include_lib("kernel/include/logger.hrl"). - --export([start_link/0]). --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - config_change/3]). --export([init/2, - websocket_init/1, - websocket_handle/2, - websocket_info/2, - terminate/3]). - --record(?MODULE, {listeners = [] :: [{proto(), inet:port_number(), pid()}]}). - --type proto() :: erldist | http. - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, #{}, []). - -%% ------------------------------------------------------------------- -%% Top-level gen_server. -%% ------------------------------------------------------------------- - -init(_) -> - process_flag(trap_exit, true), - case start_listeners() of - {ok, []} -> - ignore; - {ok, Listeners} -> - State = #?MODULE{listeners = Listeners}, - {ok, State, hibernate} - end. - -handle_call(Request, From, State) -> - ?LOG_DEBUG("CLI: unhandled call from ~0p: ~p", [From, Request]), - {reply, ok, State}. - -handle_cast(Request, State) -> - ?LOG_DEBUG("CLI: unhandled cast: ~p", [Request]), - {noreply, State}. - -handle_info(Info, State) -> - ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, #?MODULE{listeners = Listeners}) -> - stop_listeners(Listeners). - -%% ------------------------------------------------------------------- -%% HTTP listeners management. -%% ------------------------------------------------------------------- - -start_listeners() -> - case application:get_env(rabbit, cli_listeners) of - undefined -> - ?LOG_INFO("CLI: no HTTP(S) listeners started"), - {ok, []}; - {ok, Listeners} when is_list(Listeners) -> - start_listeners(Listeners, []) - end. - -start_listeners( - [{[_, _, "http" = Proto], Port} | Rest], Result) when is_integer(Port) -> - ?LOG_INFO("CLI: starting \"~s\" listener on TCP port ~b", [Proto, Port]), - Name = list_to_binary(io_lib:format("cli_listener_~s_~b", [Proto, Port])), - case start_listener(Name, Port) of - {ok, Pid} -> - Result1 = [{Proto, Port, Pid} | Result], - start_listeners(Rest, Result1); - {error, Reason} -> - ?LOG_ERROR( - "CLI: failed to start \"~s\" listener on TCP port ~b: ~0p", - [Proto, Port, Reason]), - start_listeners(Rest, Result) - end; -start_listeners([], Result) -> - Result1 = lists:reverse(Result), - {ok, Result1}. - -start_listener(Name, Port) -> - Dispatch = cowboy_router:compile([{'_', [{'_', ?MODULE, #{}}]}]), - cowboy:start_clear(Name, - [{port, Port}], - #{env => #{dispatch => Dispatch}} - ). - -stop_listeners([{Proto, Port, Pid} | Rest]) -> - ?LOG_INFO("CLI: stopping \"~s\" listener on TCP port ~b", [Proto, Port]), - _ = cowboy:stop_listener(Pid), - stop_listeners(Rest); -stop_listeners([]) -> - ok. - -config_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%% ------------------------------------------------------------------- -%% Cowboy handler. -%% ------------------------------------------------------------------- - -init(#{method := <<"GET">>} = Req, State) -> - UpgradeHeader = cowboy_req:header(<<"upgrade">>, Req), - case UpgradeHeader of - <<"websocket">> -> - {cowboy_websocket, Req, State, #{idle_timeout => 30000}}; - _ -> - case Req of - #{path := Path} - when Path =:= <<"">> orelse Path =:= <<"/index.html">> -> - Req1 = reply_with_help(Req, 200), - {ok, Req1, State}; - _ -> - Req1 = reply_with_help(Req, 404), - {ok, Req1, State} - end - end; -init(Req, State) -> - Req1 = reply_with_help(Req, 405), - {ok, Req1, State}. - -websocket_init(State) -> - process_flag(trap_exit, true), - erlang:group_leader(self(), self()), - {ok, State}. - -websocket_handle({binary, RequestBin}, State) -> - Request = binary_to_term(RequestBin), - ?LOG_DEBUG("CLI: received HTTP message from client: ~p", [Request]), - try - case handle_request(Request, State) of - {reply, Reply, State1} -> - ReplyBin = term_to_binary(Reply), - Frame1 = {binary, ReplyBin}, - {[Frame1], State1}; - {noreply, State1} -> - {ok, State1} - end - catch - Class:Reason:Stacktrace -> - Exception = {call_exception, Class, Reason, Stacktrace}, - ExceptionBin = term_to_binary(Exception), - Frame2 = {binary, ExceptionBin}, - {[Frame2], State} - end; -websocket_handle(Frame, State) -> - ?LOG_DEBUG("CLI: unhandled Websocket frame: ~p", [Frame]), - {ok, State}. - -websocket_info({frontend_request, _From, _Request} = FrontendRequest, State) -> - FrontendRequestBin = term_to_binary(FrontendRequest), - Frame = {binary, FrontendRequestBin}, - {[Frame], State}; -websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> - IoRequestBin = term_to_binary(IoRequest), - Frame = {binary, IoRequestBin}, - {[Frame], State}; -websocket_info({'EXIT', _Pid, _Reason} = Exit, State) -> - ExitBin = term_to_binary(Exit), - Frame = {binary, ExitBin}, - {[Frame, close], State}. - -terminate(Reason, _Req, State) -> - ?LOG_DEBUG("CLI: HTTP server terminating: ~0p", [Reason]), - case State of - #{backend := Backend} -> - _ = catch erlang:exit(Backend, Reason); - _ -> - ok - end, - ok. - -%% ------------------------------------------------------------------- -%% Internal functions. -%% ------------------------------------------------------------------- - -reply_with_help(Req, Code) -> - PrivDir = code:priv_dir(rabbit), - HelpFilename = filename:join(PrivDir, "cli_http_help.html"), - Body = case file:read_file(HelpFilename) of - {ok, Content} -> - Content; - {error, _} -> - <<>> - end, - cowboy_req:reply( - Code, #{<<"content-type">> => <<"text/html; charset=utf-8">>}, Body, - Req). - -handle_request({call, From, Command}, State) -> - {Ret, State1} = handle_command(Command, State), - Reply = {call_ret, From, Ret}, - {reply, Reply, State1}; -handle_request({cast, Command}, State) -> - {_, State1} = handle_command(Command, State), - {noreply, State1}. - -handle_command({run_command, ContextMap}, State) -> - Caller = self(), - case rabbit_cli_backend:run_command(ContextMap, Caller) of - {ok, Backend} = Ret -> - State1 = State#{backend => Backend}, - {Ret, State1}; - {error, _} = Error -> - {Error, State} - end; -handle_command({gen_reply, From, Reply}, State) -> - Ret = gen:reply(From, Reply), - {Ret, State}; -handle_command({send, Dest, Msg}, State) -> - Ret = erlang:send(Dest, Msg), - {Ret, State}. diff --git a/deps/rabbit/src/rabbit_cli_http_server.erl b/deps/rabbit/src/rabbit_cli_http_server.erl index c87b7b685865..bee9f26a1d6b 100644 --- a/deps/rabbit/src/rabbit_cli_http_server.erl +++ b/deps/rabbit/src/rabbit_cli_http_server.erl @@ -1,62 +1,44 @@ -module(rabbit_cli_http_server). -behaviour(gen_server). +-behaviour(cowboy_websocket). -include_lib("kernel/include/logger.hrl"). --export([start_link/1, - send_request/4, - stop/1]). +-export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, config_change/3]). +-export([init/2, + websocket_init/1, + websocket_handle/2, + websocket_info/2, + terminate/3]). -start_link(Websocket) -> - gen_server:start_link(?MODULE, #{websocket => Websocket}, []). +-record(?MODULE, {listeners = [] :: [{proto(), inet:port_number(), pid()}]}). -send_request(_Server, {cast, {send, _Dest, _Msg} = Command}, _Labet, ReqIds) -> - %% Bypass server to send messages. This is because the server might be - %% busy waiting for that message, in which case it can't receive a command - %% to send it to itself. - _ = handle_command(Command), - ReqIds; -send_request(Server, Request, Label, ReqIds) -> - gen_server:send_request(Server, Request, Label, ReqIds). +-type proto() :: erldist | http. -stop(Server) -> - gen_server:stop(Server). +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, #{}, []). %% ------------------------------------------------------------------- -%% gen_server hanling a single websocket connection. +%% Top-level gen_server. %% ------------------------------------------------------------------- -init(#{websocket := Websocket} = Args) -> +init(_) -> process_flag(trap_exit, true), - erlang:group_leader(Websocket, self()), - {ok, Args}. + case start_listeners() of + {ok, []} -> + ignore; + {ok, Listeners} -> + State = #?MODULE{listeners = Listeners}, + {ok, State, hibernate} + end. -handle_call({call, From, Command}, _From, State) -> - try - Ret = handle_command(Command), - Reply = {call_ret, From, Ret}, - {reply, {reply, Reply}, State} - catch - Class:Reason:Stacktrace -> - Exception = {call_exception, Class, Reason, Stacktrace}, - {reply, {reply, Exception}, State} - end; -handle_call({cast, Command}, _From, State) -> - try - _ = handle_command(Command), - {reply, noreply, State} - catch - Class:Reason:Stacktrace -> - Exception = {call_exception, Class, Reason, Stacktrace}, - {reply, {reply, Exception}, State} - end; handle_call(Request, From, State) -> ?LOG_DEBUG("CLI: unhandled call from ~0p: ~p", [From, Request]), {reply, ok, State}. @@ -69,13 +51,169 @@ handle_info(Info, State) -> ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), {noreply, State}. -terminate(_Reason, _State) -> +terminate(_Reason, #?MODULE{listeners = Listeners}) -> + stop_listeners(Listeners). + +%% ------------------------------------------------------------------- +%% HTTP listeners management. +%% ------------------------------------------------------------------- + +start_listeners() -> + case application:get_env(rabbit, cli_listeners) of + undefined -> + ?LOG_INFO("CLI: no HTTP(S) listeners started"), + {ok, []}; + {ok, Listeners} when is_list(Listeners) -> + start_listeners(Listeners, []) + end. + +start_listeners( + [{[_, _, "http" = Proto], Port} | Rest], Result) when is_integer(Port) -> + ?LOG_INFO("CLI: starting \"~s\" listener on TCP port ~b", [Proto, Port]), + Name = list_to_binary(io_lib:format("cli_listener_~s_~b", [Proto, Port])), + case start_listener(Name, Port) of + {ok, Pid} -> + Result1 = [{Proto, Port, Pid} | Result], + start_listeners(Rest, Result1); + {error, Reason} -> + ?LOG_ERROR( + "CLI: failed to start \"~s\" listener on TCP port ~b: ~0p", + [Proto, Port, Reason]), + start_listeners(Rest, Result) + end; +start_listeners([], Result) -> + Result1 = lists:reverse(Result), + {ok, Result1}. + +start_listener(Name, Port) -> + Dispatch = cowboy_router:compile([{'_', [{'_', ?MODULE, #{}}]}]), + cowboy:start_clear(Name, + [{port, Port}], + #{env => #{dispatch => Dispatch}} + ). + +stop_listeners([{Proto, Port, Pid} | Rest]) -> + ?LOG_INFO("CLI: stopping \"~s\" listener on TCP port ~b", [Proto, Port]), + _ = cowboy:stop_listener(Pid), + stop_listeners(Rest); +stop_listeners([]) -> ok. config_change(_OldVsn, State, _Extra) -> {ok, State}. -handle_command({rpc, Module, Function, Args}) -> - erlang:apply(Module, Function, Args); -handle_command({send, Dest, Msg}) -> - erlang:send(Dest, Msg). +%% ------------------------------------------------------------------- +%% Cowboy handler. +%% ------------------------------------------------------------------- + +init(#{method := <<"GET">>} = Req, State) -> + UpgradeHeader = cowboy_req:header(<<"upgrade">>, Req), + case UpgradeHeader of + <<"websocket">> -> + {cowboy_websocket, Req, State, #{idle_timeout => 30000}}; + _ -> + case Req of + #{path := Path} + when Path =:= <<"">> orelse Path =:= <<"/index.html">> -> + Req1 = reply_with_help(Req, 200), + {ok, Req1, State}; + _ -> + Req1 = reply_with_help(Req, 404), + {ok, Req1, State} + end + end; +init(Req, State) -> + Req1 = reply_with_help(Req, 405), + {ok, Req1, State}. + +websocket_init(State) -> + process_flag(trap_exit, true), + erlang:group_leader(self(), self()), + {ok, State}. + +websocket_handle({binary, RequestBin}, State) -> + Request = binary_to_term(RequestBin), + ?LOG_DEBUG("CLI: received HTTP message from client: ~p", [Request]), + try + case handle_request(Request, State) of + {reply, Reply, State1} -> + ReplyBin = term_to_binary(Reply), + Frame1 = {binary, ReplyBin}, + {[Frame1], State1}; + {noreply, State1} -> + {ok, State1} + end + catch + Class:Reason:Stacktrace -> + Exception = {call_exception, Class, Reason, Stacktrace}, + ExceptionBin = term_to_binary(Exception), + Frame2 = {binary, ExceptionBin}, + {[Frame2], State} + end; +websocket_handle(Frame, State) -> + ?LOG_DEBUG("CLI: unhandled Websocket frame: ~p", [Frame]), + {ok, State}. + +websocket_info({frontend_request, _From, _Request} = FrontendRequest, State) -> + FrontendRequestBin = term_to_binary(FrontendRequest), + Frame = {binary, FrontendRequestBin}, + {[Frame], State}; +websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> + IoRequestBin = term_to_binary(IoRequest), + Frame = {binary, IoRequestBin}, + {[Frame], State}; +websocket_info({'EXIT', _Pid, _Reason} = Exit, State) -> + ExitBin = term_to_binary(Exit), + Frame = {binary, ExitBin}, + {[Frame, close], State}. + +terminate(Reason, _Req, State) -> + ?LOG_DEBUG("CLI: HTTP server terminating: ~0p", [Reason]), + case State of + #{backend := Backend} -> + _ = catch erlang:exit(Backend, Reason); + _ -> + ok + end, + ok. + +%% ------------------------------------------------------------------- +%% Internal functions. +%% ------------------------------------------------------------------- + +reply_with_help(Req, Code) -> + PrivDir = code:priv_dir(rabbit), + HelpFilename = filename:join(PrivDir, "cli_http_help.html"), + Body = case file:read_file(HelpFilename) of + {ok, Content} -> + Content; + {error, _} -> + <<>> + end, + cowboy_req:reply( + Code, #{<<"content-type">> => <<"text/html; charset=utf-8">>}, Body, + Req). + +handle_request({call, From, Command}, State) -> + {Ret, State1} = handle_command(Command, State), + Reply = {call_ret, From, Ret}, + {reply, Reply, State1}; +handle_request({cast, Command}, State) -> + {_, State1} = handle_command(Command, State), + {noreply, State1}. + +handle_command({run_command, ContextMap}, State) -> + Caller = self(), + case rabbit_cli_backend:run_command(ContextMap, Caller) of + {ok, Backend} = Ret -> + State1 = State#{backend => Backend}, + {Ret, State1}; + {error, _} = Error -> + {Error, State} + end; +handle_command({gen_reply, From, Reply}, State) -> + Ret = gen:reply(From, Reply), + {Ret, State}; +handle_command({send, Dest, Msg}, State) -> + Ret = erlang:send(Dest, Msg), + {Ret, State}. diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl index 552fd2472283..4de9df3032d9 100644 --- a/deps/rabbit/src/rabbit_cli_transport.erl +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -1,62 +1,53 @@ -module(rabbit_cli_transport). --behaviour(gen_server). --export([connect/1, - rpc/4]). --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - config_change/3]). +-include_lib("kernel/include/logger.hrl"). --record(http, {uri :: uri_string:uri_map(), - conn :: pid(), - stream :: gun:stream_ref(), - stream_ready = false :: boolean(), - pending = [] :: [any()], - pending_io_requests = #{} :: map(), - group_leader :: pid() - }). +-export([connect/0, connect/1, + get_client_info/1, + run_command/2, + gen_reply/3, + send/3]). -connect(#{arg_map := #{node := NodenameOrUri}} = Context) -> - case re:run(NodenameOrUri, "://", [{capture, none}]) of - nomatch -> - connect_using_erldist(Context); - match -> - connect_using_transport(Context) - end; -connect(Context) -> - connect_using_erldist(Context). +-record(?MODULE, {type :: erldist | http, + peer :: atom() | pid()}). -rpc(Nodename, Mod, Func, Args) when is_atom(Nodename) -> - rpc_using_erldist(Nodename, Mod, Func, Args); -rpc(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> - rpc_using_transport(TransportPid, Mod, Func, Args). +connect() -> + Nodename = guess_rabbitmq_nodename(), + connect(erldist, Nodename). -%% ------------------------------------------------------------------- -%% Erlang distribution. -%% ------------------------------------------------------------------- +connect(NodenameOrUri) -> + Proto = determine_proto(NodenameOrUri), + connect(Proto, NodenameOrUri). -connect_using_erldist(#{arg_map := #{node := Nodename}}) -> - do_connect_using_erldist(Nodename); -connect_using_erldist(_Context) -> - GuessedNodename = guess_rabbitmq_nodename(), - do_connect_using_erldist(GuessedNodename). - -do_connect_using_erldist(Nodename) -> +connect(erldist = Proto, Nodename) -> maybe Nodename1 = complete_nodename(Nodename), - {ok, _} ?= net_kernel:start( - undefined, #{name_domain => shortnames}), + ?LOG_DEBUG( + "CLI: connect to node ~s using Erlang distribution", + [Nodename1]), + + %% FIXME: Handle short vs. long names. + {ok, _} ?= net_kernel:start(undefined, #{name_domain => shortnames}), %% Can we reach the remote node? case net_kernel:connect_node(Nodename1) of true -> - {ok, Nodename1}; + Connection = #?MODULE{type = Proto, + peer = Nodename1}, + {ok, Connection}; false -> {error, noconnection} end + end; +connect(http = Proto, Uri) -> + maybe + ?LOG_DEBUG( + "CLI: connect to URI ~s using HTTP", + [Uri]), + {ok, Client} ?= rabbit_cli_http_client:start_link(Uri), + Connection = #?MODULE{type = Proto, + peer = Client}, + {ok, Connection} end. guess_rabbitmq_nodename() -> @@ -79,6 +70,14 @@ guess_rabbitmq_nodename() -> "rabbit" end. +determine_proto(NodenameOrUri) -> + case re:run(NodenameOrUri, "://", [{capture, none}]) of + nomatch -> + erldist; + match -> + http + end. + complete_nodename(Nodename) -> case re:run(Nodename, "@", [{capture, none}]) of nomatch -> @@ -88,129 +87,23 @@ complete_nodename(Nodename) -> list_to_atom(Nodename) end. -rpc_using_erldist(Nodename, Mod, Func, Args) -> - erpc:call(Nodename, Mod, Func, Args). - -%% ------------------------------------------------------------------- -%% HTTP(S) transport. -%% ------------------------------------------------------------------- - -connect_using_transport(Context) -> - gen_server:start_link(?MODULE, Context, []). - -rpc_using_transport(TransportPid, Mod, Func, Args) when is_pid(TransportPid) -> - gen_server:call(TransportPid, {rpc, {Mod, Func, Args}}). - -init(#{arg_map := #{node := Uri}, group_leader := GL}) -> - maybe - {ok, _} ?= application:ensure_all_started(gun), - #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), - {ok, ConnPid} ?= gun:open(Host, Port), - State = #http{uri = UriMap, - group_leader = GL, - conn = ConnPid}, - %logger:alert("Transport: State=~p", [State]), - {ok, State} - end. - -handle_call( - Request, From, - #http{stream_ready = true} = State) -> - %% HTTP message to the server side. - send_call(Request, From, State), - {noreply, State}; -handle_call( - Request, From, - #http{stream_ready = false, pending = Pending} = State) -> - %logger:alert("Transport(call): ~p", [Request]), - State1 = State#http{pending = [{From, Request} | Pending]}, - {noreply, State1}; -handle_call(_Request, _From, State) -> - %logger:alert("Transport(call): ~p", [_Request]), - {reply, ok, State}. - -handle_cast(_Request, State) -> - %logger:alert("Transport(cast): ~p", [_Request]), - {noreply, State}. - -handle_info( - {gun_up, ConnPid, _}, - #http{conn = ConnPid} = State) -> - %logger:alert("Transport(info): Conn up"), - StreamRef = gun:ws_upgrade(ConnPid, "/", []), - State1 = State#http{stream = StreamRef}, - {noreply, State1}; -handle_info( - {gun_upgrade, ConnPid, StreamRef, _Frames, _}, - #http{conn = ConnPid, stream = StreamRef, pending = Pending} = State) -> - %logger:alert("Transport(info): WS upgraded, ~p", [_Frames]), - State1 = State#http{stream_ready = true, pending = []}, - Pending1 = lists:reverse(Pending), - lists:foreach( - fun({From, Request}) -> - send_call(Request, From, State1) - end, Pending1), - {noreply, State1}; -handle_info( - {gun_ws, ConnPid, StreamRef, {binary, ReplyBin}}, - #http{conn = ConnPid, - stream = StreamRef, - group_leader = GL, - pending_io_requests = Pending} = State) -> - %% HTTP message from the server side. - Reply = binary_to_term(ReplyBin), - State1 = case Reply of - % {io_call, From, Msg} -> - % %logger:alert("IO call from WS: ~p -> ~p", [Msg, From]), - % Ret = gen_server:call(IO, Msg), - % RequestBin = term_to_binary({io_reply, From, Ret}), - % Frame = {binary, RequestBin}, - % gun:ws_send(ConnPid, StreamRef, Frame); - % {io_cast, Msg} -> - % %logger:alert("IO cast from WS: ~p", [Msg]), - % gen_server:cast(IO, Msg); - {msg, group_leader, {io_request, RemoteFrom, ReplyAs, Request} = _Msg} -> - % logger:alert("Message from WS: ~p", [Msg]), - Ref = erlang:make_ref(), - IoRequest1 = {io_request, self(), Ref, Request}, - GL ! IoRequest1, - Pending1 = Pending#{Ref => {RemoteFrom, ReplyAs}}, - State#http{pending_io_requests = Pending1}; - {ret, From, Ret} -> - %logger:alert("Reply from WS: ~p -> ~p", [Ret, From]), - gen_server:reply(From, Ret), - State; - _Other -> - %logger:alert("Reply from WS: ~p", [_Other]), - State - end, - {noreply, State1}; -handle_info( - {io_reply, ReplyAs, Reply} = _IoReply, - #http{conn = ConnPid, - stream = StreamRef, - pending_io_requests = Pending} = State) -> - % logger:alert("io_reply to WS: ~p", [IoReply]), - {RemoteFrom, RemoteReplyAs} = maps:get(ReplyAs, Pending), - Msg = {io_reply, RemoteReplyAs, Reply}, - RequestBin = term_to_binary({msg, RemoteFrom, Msg}), - Frame = {binary, RequestBin}, - gun:ws_send(ConnPid, StreamRef, Frame), - - Pending1 = maps:remove(ReplyAs, Pending), - State1 = State#http{pending_io_requests = Pending1}, - {noreply, State1}; -handle_info(_Info, State) -> - %logger:alert("Transport(info): ~p", [_Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -config_change(_OldVsn, State, _Extra) -> - {ok, State}. - -send_call(Request, From, #http{conn = ConnPid, stream = StreamRef}) -> - RequestBin = term_to_binary({call, From, Request}), - Frame = {binary, RequestBin}, - gun:ws_send(ConnPid, StreamRef, Frame). +get_client_info(#?MODULE{type = Proto}) -> + {ok, Hostname} = inet:gethostname(), + #{hostname => Hostname, + proto => Proto}. + +run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> + Caller = self(), + erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); +run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> + rabbit_cli_http_client:run_command(Client, ContextMap). + +gen_reply(#?MODULE{type = erldist}, From, Reply) -> + gen:reply(From, Reply); +gen_reply(#?MODULE{type = http, peer = Client}, From, Reply) -> + rabbit_cli_http_client:gen_reply(Client, From, Reply). + +send(#?MODULE{type = erldist}, Dest, Msg) -> + erlang:send(Dest, Msg); +send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> + rabbit_cli_http_client:send(Client, Dest, Msg). diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl deleted file mode 100644 index f85ff37b7242..000000000000 --- a/deps/rabbit/src/rabbit_cli_transport2.erl +++ /dev/null @@ -1,109 +0,0 @@ --module(rabbit_cli_transport2). - --include_lib("kernel/include/logger.hrl"). - --export([connect/0, connect/1, - get_client_info/1, - run_command/2, - gen_reply/3, - send/3]). - --record(?MODULE, {type :: erldist | http, - peer :: atom() | pid()}). - -connect() -> - Nodename = guess_rabbitmq_nodename(), - connect(erldist, Nodename). - -connect(NodenameOrUri) -> - Proto = determine_proto(NodenameOrUri), - connect(Proto, NodenameOrUri). - -connect(erldist = Proto, Nodename) -> - maybe - Nodename1 = complete_nodename(Nodename), - ?LOG_DEBUG( - "CLI: connect to node ~s using Erlang distribution", - [Nodename1]), - - %% FIXME: Handle short vs. long names. - {ok, _} ?= net_kernel:start(undefined, #{name_domain => shortnames}), - - %% Can we reach the remote node? - case net_kernel:connect_node(Nodename1) of - true -> - Connection = #?MODULE{type = Proto, - peer = Nodename1}, - {ok, Connection}; - false -> - {error, noconnection} - end - end; -connect(http = Proto, Uri) -> - maybe - ?LOG_DEBUG( - "CLI: connect to URI ~s using HTTP", - [Uri]), - {ok, Client} ?= rabbit_cli_http_client:start_link(Uri), - Connection = #?MODULE{type = Proto, - peer = Client}, - {ok, Connection} - end. - -guess_rabbitmq_nodename() -> - case net_adm:names() of - {ok, NamesAndPorts} -> - Names0 = [Name || {Name, _Port} <- NamesAndPorts], - Names1 = lists:sort(Names0), - Names2 = lists:filter( - fun - ("rabbit" ++ _) -> true; - (_) -> false - end, Names1), - case Names2 of - [First | _] -> - First; - [] -> - "rabbit" - end; - {error, address} -> - "rabbit" - end. - -determine_proto(NodenameOrUri) -> - case re:run(NodenameOrUri, "://", [{capture, none}]) of - nomatch -> - erldist; - match -> - http - end. - -complete_nodename(Nodename) -> - case re:run(Nodename, "@", [{capture, none}]) of - nomatch -> - {ok, ThisHost} = inet:gethostname(), - list_to_atom(Nodename ++ "@" ++ ThisHost); - match -> - list_to_atom(Nodename) - end. - -get_client_info(#?MODULE{type = Proto}) -> - {ok, Hostname} = inet:gethostname(), - #{hostname => Hostname, - proto => Proto}. - -run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> - Caller = self(), - erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); -run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> - rabbit_cli_http_client:run_command(Client, ContextMap). - -gen_reply(#?MODULE{type = erldist}, From, Reply) -> - gen:reply(From, Reply); -gen_reply(#?MODULE{type = http, peer = Client}, From, Reply) -> - rabbit_cli_http_client:gen_reply(Client, From, Reply). - -send(#?MODULE{type = erldist}, Dest, Msg) -> - erlang:send(Dest, Msg); -send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> - rabbit_cli_http_client:send(Client, Dest, Msg). diff --git a/deps/rabbit/src/rabbit_cli_ws.erl b/deps/rabbit/src/rabbit_cli_ws.erl deleted file mode 100644 index e103bc60d1a5..000000000000 --- a/deps/rabbit/src/rabbit_cli_ws.erl +++ /dev/null @@ -1,114 +0,0 @@ --module(rabbit_cli_ws). --behaviour(gen_server). --behaviour(cowboy_websocket). - --export([start_link/0]). --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - config_change/3]). --export([init/2, - websocket_init/1, - websocket_handle/2, - websocket_info/2, - terminate/3]). - -start_link() -> - gen_server:start_link(?MODULE, #{}, []). - -init(_) -> - process_flag(trap_exit, true), - Dispatch = cowboy_router:compile( - [{'_', [{'_', ?MODULE, #{}}]}]), - {ok, _} = cowboy:start_clear(cli_ws_listener, - [{port, 8080}], - #{env => #{dispatch => Dispatch}} - ), - {ok, ok}. - -handle_call(_Request, _From, State) -> - {reply, ok, State}. - -handle_cast(_Request, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - logger:alert("WS/gen_server: ~p", [_Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - logger:alert("WS/gen_server(terminate): ~p", [_Reason]), - ok = cowboy:stop_listener(cli_ws_listener), - ok. - -config_change(_OldVsn, State, _Extra) -> - {ok, State}. - -init(Req, State) -> - logger:alert("WS: Req=~p", [Req]), - {cowboy_websocket, Req, State, #{idle_timeout => 30000}}. - -websocket_init(State) -> - {ok, Runner} = rabbit_cli_ws_runner:start_link(self()), - State1 = State#{runner => Runner}, - {ok, State1}. - -websocket_handle({binary, RequestBin}, State) -> - %% HTTP message from the client side. - Request = binary_to_term(RequestBin), - case Request of - % {io_reply, From, Ret} -> - % From ! Ret, - % {ok, State}; - {call, From, Call} -> - handle_ws_call(Call, From, State), - {ok, State}; - {msg, To, Msg} -> - % logger:alert("Message to ~0p: ~p", [To, Msg]), - To ! Msg, - {ok, State}; - _ -> - logger:alert("Unknown request: ~p", [Request]), - ReplyBin = term_to_binary({error, Request}), - Frame = {binary, ReplyBin}, - {[Frame], State} - end; -websocket_handle(_Frame, State) -> - logger:alert("Frame: ~p", [_Frame]), - {ok, State}. - -% websocket_info({io_call, _From, _Msg} = Call, State) -> -% ReplyBin = term_to_binary(Call), -% Frame = {binary, ReplyBin}, -% {[Frame], State}; -% websocket_info({io_cast, _Msg} = Call, State) -> -% ReplyBin = term_to_binary(Call), -% Frame = {binary, ReplyBin}, -% {[Frame], State}; -websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> - % logger:alert("WS/cowboy: ~p", [IoRequest]), - ReplyBin = term_to_binary({msg, group_leader, IoRequest}), - Frame = {binary, ReplyBin}, - {[Frame], State}; -websocket_info({reply, Ret, From}, State) -> - logger:alert("WS/cowboy: ~p", [Ret]), - ReplyBin = term_to_binary({ret, From, Ret}), - Frame = {binary, ReplyBin}, - {[Frame], State}; -websocket_info(_Info, State) -> - logger:alert("WS/cowboy: ~p", [_Info]), - {ok, State}. - -terminate(_Reason, _Req, #{runner := Runner}) -> - rabbit_cli_ws_runner:stop(Runner), - receive - {'EXIT', Runner, _} -> - ok - end, - ok. - -handle_ws_call(Call, From, #{runner := Runner}) -> - logger:alert("Call: ~p", [Call]), - gen_server:cast(Runner, {Call, From}). diff --git a/deps/rabbit/src/rabbit_cli_ws_runner.erl b/deps/rabbit/src/rabbit_cli_ws_runner.erl deleted file mode 100644 index d5d38cc04711..000000000000 --- a/deps/rabbit/src/rabbit_cli_ws_runner.erl +++ /dev/null @@ -1,62 +0,0 @@ --module(rabbit_cli_ws_runner). --behaviour(gen_server). - --export([start_link/1, - stop/1]). --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - config_change/3]). - -start_link(WS) -> - gen_server:start_link(?MODULE, #{ws => WS}, []). - -stop(Runner) -> - gen_server:stop(Runner). - -init(#{ws := WS} = Args) -> - process_flag(trap_exit, true), - _GL = erlang:group_leader(), - erlang:group_leader(WS, self()), - % spawn(fun() -> - % logger:alert("GL: ~0p -> ~0p", [GL, erlang:group_leader()]), - % io:format("GL: ~0p -> ~0p~n", [GL, erlang:group_leader()]), - % logger:alert("done with GL switch") - % end), - {ok, Args}. - -handle_call(_Request, _From, State) -> - logger:alert("Runner(call): ~p", [_Request]), - {reply, ok, State}. - -handle_cast( - {{rpc, {Mod, Func, Args}}, From}, - #{ws := WS} = State) -> - try - Ret = erlang:apply(Mod, Func, Args), - logger:alert("Runner(rpc): ~p", [Ret]), - WS ! {reply, Ret, From}, - {noreply, State} - catch - Class:Reason:Stacktrace -> - Ex = {exception, Class, Reason, Stacktrace}, - WS ! {reply, Ex, From}, - {noreply, State} - end; -handle_cast(_Request, State) -> - logger:alert("Runner(cast): ~p", [_Request]), - {noreply, State}. - -handle_info({'EXIT', WS, _Reason}, #{ws := WS} = State) -> - {stop, State}; -handle_info(_Info, State) -> - logger:alert("Runner/gen_server: ~p, ~p", [_Info, State]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -config_change(_OldVsn, State, _Extra) -> - {ok, State}. From c01090158a306cc8fbf8ea98ee6ffeeb04901b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 15:15:36 +0200 Subject: [PATCH 48/51] Replace old CLIs by new CLI legacy mode --- deps/rabbit/scripts/rabbitmq-diagnostics | 37 ----- deps/rabbit/scripts/rabbitmq-diagnostics.bat | 62 ------- deps/rabbit/scripts/rabbitmq-plugins | 23 --- deps/rabbit/scripts/rabbitmq-plugins.bat | 56 ------- deps/rabbit/scripts/rabbitmq-queues | 23 --- deps/rabbit/scripts/rabbitmq-queues.bat | 56 ------- deps/rabbit/scripts/rabbitmq-streams | 24 --- deps/rabbit/scripts/rabbitmq-streams.bat | 55 ------- deps/rabbit/scripts/rabbitmq-upgrade | 23 --- deps/rabbit/scripts/rabbitmq-upgrade.bat | 55 ------- deps/rabbit/scripts/rabbitmqctl | 164 ------------------- deps/rabbit/scripts/rabbitmqctl.bat | 56 ------- deps/rabbit/src/rabbit_cli_frontend.erl | 28 +++- deps/rabbit_common/mk/rabbitmq-dist.mk | 19 +++ 14 files changed, 46 insertions(+), 635 deletions(-) delete mode 100755 deps/rabbit/scripts/rabbitmq-diagnostics delete mode 100644 deps/rabbit/scripts/rabbitmq-diagnostics.bat delete mode 100755 deps/rabbit/scripts/rabbitmq-plugins delete mode 100644 deps/rabbit/scripts/rabbitmq-plugins.bat delete mode 100755 deps/rabbit/scripts/rabbitmq-queues delete mode 100644 deps/rabbit/scripts/rabbitmq-queues.bat delete mode 100755 deps/rabbit/scripts/rabbitmq-streams delete mode 100644 deps/rabbit/scripts/rabbitmq-streams.bat delete mode 100755 deps/rabbit/scripts/rabbitmq-upgrade delete mode 100644 deps/rabbit/scripts/rabbitmq-upgrade.bat delete mode 100755 deps/rabbit/scripts/rabbitmqctl delete mode 100644 deps/rabbit/scripts/rabbitmqctl.bat diff --git a/deps/rabbit/scripts/rabbitmq-diagnostics b/deps/rabbit/scripts/rabbitmq-diagnostics deleted file mode 100755 index c874df8cdf3f..000000000000 --- a/deps/rabbit/scripts/rabbitmq-diagnostics +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -maybe_noinput='noinput' - -case "$@" in - *observer*) - maybe_noinput='input' - ;; - *remote_shell*) - maybe_noinput='input' - ;; - *) - maybe_noinput='noinput' - ;; -esac - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-diagnostics "$maybe_noinput" "$@" diff --git a/deps/rabbit/scripts/rabbitmq-diagnostics.bat b/deps/rabbit/scripts/rabbitmq-diagnostics.bat deleted file mode 100644 index bb29099d14da..000000000000 --- a/deps/rabbit/scripts/rabbitmq-diagnostics.bat +++ /dev/null @@ -1,62 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -if "%1"=="remote_shell" ( - set ERL_CMD=werl.exe -) else ( - set ERL_CMD=erl.exe -) - -REM Note: do NOT add -noinput because "observer" depends on it -"!ERLANG_HOME!\bin\!ERL_CMD!" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-diagnostics" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-plugins b/deps/rabbit/scripts/rabbitmq-plugins deleted file mode 100755 index 3e1ea1d3360b..000000000000 --- a/deps/rabbit/scripts/rabbitmq-plugins +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-plugins 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-plugins.bat b/deps/rabbit/scripts/rabbitmq-plugins.bat deleted file mode 100644 index 553ba7a0b558..000000000000 --- a/deps/rabbit/scripts/rabbitmq-plugins.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off - -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "!TDP0!\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-plugins" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-queues b/deps/rabbit/scripts/rabbitmq-queues deleted file mode 100755 index 79e9cef052af..000000000000 --- a/deps/rabbit/scripts/rabbitmq-queues +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-queues 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-queues.bat b/deps/rabbit/scripts/rabbitmq-queues.bat deleted file mode 100644 index b38a1332fbf6..000000000000 --- a/deps/rabbit/scripts/rabbitmq-queues.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-queues" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-streams b/deps/rabbit/scripts/rabbitmq-streams deleted file mode 100755 index b04e23ba9dbf..000000000000 --- a/deps/rabbit/scripts/rabbitmq-streams +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-streams 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-streams.bat b/deps/rabbit/scripts/rabbitmq-streams.bat deleted file mode 100644 index e34359cea4a2..000000000000 --- a/deps/rabbit/scripts/rabbitmq-streams.bat +++ /dev/null @@ -1,55 +0,0 @@ -@echo off - -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-streams" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-upgrade b/deps/rabbit/scripts/rabbitmq-upgrade deleted file mode 100755 index 7a067e3bd7b5..000000000000 --- a/deps/rabbit/scripts/rabbitmq-upgrade +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-upgrade 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-upgrade.bat b/deps/rabbit/scripts/rabbitmq-upgrade.bat deleted file mode 100644 index d0229f7a581f..000000000000 --- a/deps/rabbit/scripts/rabbitmq-upgrade.bat +++ /dev/null @@ -1,55 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-upgrade" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal diff --git a/deps/rabbit/scripts/rabbitmqctl b/deps/rabbit/scripts/rabbitmqctl deleted file mode 100755 index 2a3dac189c59..000000000000 --- a/deps/rabbit/scripts/rabbitmqctl +++ /dev/null @@ -1,164 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -# Uncomment for debugging -# echo "\$# : $#" -# echo "\$0: $0" -# echo "\$1: $1" -# echo "\$2: $2" -# echo "\$3: $3" -# echo "\$4: $4" -# echo "\$5: $5" -# set -x - -_tmp_help_requested='false' - -for _tmp_argument in "$@" -do - if [ "$_tmp_argument" = '--help' ] - then - _tmp_help_requested='true' - break - fi -done - -if [ "$1" = 'help' ] || [ "$_tmp_help_requested" = 'true' ] -then - unset _tmp_help_requested - # In this case, we do not require input and can exit early since - # help was requested - # - run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmqctl 'noinput' "$@" - exit "$?" -fi - -unset _tmp_help_requested - -maybe_noinput='noinput' - -case "$@" in - *add_user*) - if [ "$#" -eq 2 ] - then - # In this case, input is required to provide the password: - # - # rabbitmqctl add_user bob - # - maybe_noinput='input' - elif [ "$#" -eq 3 ] - then - # In these cases, input depends on the arguments provided: - # - # rabbitmqctl add_user bob --pre-hashed-password (input needed) - # rabbitmqctl add_user bob bobpassword (NO input needed) - # rabbitmqctl add_user --pre-hashed-password bob (input needed) - # - for _tmp_argument in "$@" - do - if [ "$_tmp_argument" = '--pre-hashed-password' ] - then - maybe_noinput='input' - break - fi - done - elif [ "$#" -gt 3 ] - then - # If there are 4 or more arguments, no input is needed: - # - # rabbitmqctl add_user bob --pre-hashed-password HASHVALUE - # rabbitmqctl add_user bob bobpassword IGNORED - # rabbitmqctl add_user --pre-hashed-password bob HASHVALUE - # - maybe_noinput='noinput' - fi - ;; - *authenticate_user*) - if [ "$#" -eq 2 ] - then - # In this case, input is required to provide the password: - # - # rabbitmqctl authenticate_user bob - # - maybe_noinput='input' - elif [ "$#" -gt 2 ] - then - # If there are 2 or more arguments, no input is needed: - # - maybe_noinput='noinput' - fi - ;; - *change_password*) - maybe_noinput='input' - if [ "$#" -gt 2 ] - then - # If there are 3 or more arguments, no input is needed: - # - # rabbitmqctl change_password sue foobar - # rabbitmqctl change_password sue newpassword IGNORED - # - maybe_noinput='noinput' - fi - ;; - *decode*|*encode*) - # It is unlikely that these commands will be run in a shell script loop - # with redirection, so always assume that stdin input is needed - # - maybe_noinput='input' - ;; - *eval*) - if [ "$#" -eq 1 ] - then - # If there is only one argument, 'eval', then input is required - # - # rabbitmqctl eval - # - maybe_noinput='input' - fi - ;; - *hash_password*) - if [ "$#" -eq 1 ] - then - # If there is only one argument, 'hash_password', then input is required - # - # rabbitmqctl hash_password - # - maybe_noinput='input' - fi - ;; - *import_definitions*) - if [ "$#" -eq 1 ] - then - # If there is only one argument, 'import_definitions', then input is required - # - # rabbitmqctl import_definitions - # - maybe_noinput='input' - fi - ;; - *) - maybe_noinput='noinput' - ;; -esac - -unset _tmp_argument - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmqctl "$maybe_noinput" "$@" diff --git a/deps/rabbit/scripts/rabbitmqctl.bat b/deps/rabbit/scripts/rabbitmqctl.bat deleted file mode 100644 index 9afe78c6f1bc..000000000000 --- a/deps/rabbit/scripts/rabbitmqctl.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmqctl" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 4b1224e004ca..06becb1b5e77 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -74,10 +74,33 @@ flush_log_messages() -> run_cli(ScriptName, Args) -> ProgName0 = filename:basename(ScriptName, ".bat"), ProgName1 = filename:basename(ProgName0, ".escript"), + case is_legacy_progname(ProgName1) of + false -> + run_cli(ScriptName, ProgName1, Args); + true -> + run_legacy_cli(Args) + end. + +is_legacy_progname("rabbitmqctl") -> + true; +is_legacy_progname("rabbitmq-diagnostics") -> + true; +is_legacy_progname("rabbitmq-plugins") -> + true; +is_legacy_progname("rabbitmq-queues") -> + true; +is_legacy_progname("rabbitmq-streams") -> + true; +is_legacy_progname("rabbitmq-upgrade") -> + true; +is_legacy_progname(_Progname) -> + false. + +run_cli(ScriptName, ProgName, Args) -> Terminal = collect_terminal_info(), configure_signal_handler(), Priv = #?MODULE{scriptname = ScriptName}, - Context = #rabbit_cli{progname = ProgName1, + Context = #rabbit_cli{progname = ProgName, args = Args, os = os:type(), env = os:env(), @@ -85,6 +108,9 @@ run_cli(ScriptName, Args) -> priv = Priv}, init_local_args(Context). +run_legacy_cli(Args) -> + 'Elixir.RabbitMQCtl':main(Args). + collect_terminal_info() -> IoOpts = io:getopts(), Term = eterminfo:get_term_type_or_default(), diff --git a/deps/rabbit_common/mk/rabbitmq-dist.mk b/deps/rabbit_common/mk/rabbitmq-dist.mk index b38ab383ba18..5f83b5a92bc4 100644 --- a/deps/rabbit_common/mk/rabbitmq-dist.mk +++ b/deps/rabbit_common/mk/rabbitmq-dist.mk @@ -200,10 +200,23 @@ do-dist:: $(DIST_EZS) $(verbose) unwanted='$(filter-out $(DIST_EZS) $(EXTRA_DIST_EZS), \ $(wildcard $(DIST_DIR)/*))'; \ test -z "$$unwanted" || (echo " RM $$unwanted" && rm -rf $$unwanted) + $(verbose) unzip -d "$(DIST_DIR)" \ + -x 'csv*' \ + -x 'json*' \ + -x 'stdout_formatter*' \ + -o \ + $(CLI_ESCRIPTS_DIR)/rabbitmqctl CLI_SCRIPTS_LOCK = $(CLI_SCRIPTS_DIR).lock CLI_ESCRIPTS_LOCK = $(CLI_ESCRIPTS_DIR).lock +LEGACY_CLI_COMMANDS := rabbitmqctl \ + rabbitmq-diagnostics \ + rabbitmq-plugins \ + rabbitmq-queues \ + rabbitmq-streams \ + rabbitmq-upgrade + ifeq ($(MAKELEVEL),0) ifneq ($(filter-out rabbit_common amqp10_common rabbitmq_stream_common,$(PROJECT)),) # These do not depend on 'rabbit' as DEPS but may as TEST_DEPS. @@ -223,6 +236,12 @@ install-cli-scripts: | $(CLI_SCRIPTS_DIR) test -d "$(DEPS_DIR)/rabbit/scripts"; \ $(call maybe_flock,$(CLI_SCRIPTS_LOCK), \ cp -a $(DEPS_DIR)/rabbit/scripts/* $(CLI_SCRIPTS_DIR)/) + $(verbose) \ + $(foreach cmd, $(LEGACY_CLI_COMMANDS), \ + ln -sf rabbitmq.escript $(CLI_SCRIPTS_DIR)/$(cmd).escript;) + $(verbose) \ + $(foreach cmd, $(LEGACY_CLI_COMMANDS), \ + ln -sf rabbitmq $(CLI_SCRIPTS_DIR)/$(cmd);) install-cli-escripts: | $(CLI_ESCRIPTS_DIR) $(gen_verbose) $(call maybe_flock,$(CLI_ESCRIPTS_LOCK), \ From 84ebb4ce9ffaa182bb76c44bd8c054b2702f9c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 16:26:26 +0200 Subject: [PATCH 49/51] Git: Ignore rabbitmq CLI --- deps/rabbit/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deps/rabbit/.gitignore b/deps/rabbit/.gitignore index 9e124a080135..0c704442d2aa 100644 --- a/deps/rabbit/.gitignore +++ b/deps/rabbit/.gitignore @@ -4,3 +4,6 @@ [Bb]in/ [Oo]bj/ + +/scripts/rabbitmq +/scripts/rabbitmq.escript From b6c496be042d425437a48969909c16a52cae0e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 20:58:03 +0200 Subject: [PATCH 50/51] Implement --version --- deps/rabbit/src/rabbit_cli_backend.erl | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index 412583cc7027..da69d82df118 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -179,6 +179,11 @@ handle_event( #rabbit_cli{arg_map = #{help := true}} = Context) -> display_help(Context), {stop, {shutdown, ok}, Context}; +handle_event( + internal, run_command, command_parsed, + #rabbit_cli{arg_map = #{version := true}} = Context) -> + display_version(Context), + {stop, {shutdown, ok}, Context}; handle_event(internal, run_command, command_parsed, Context) -> Ret = do_run_command(Context), {stop, {shutdown, Ret}, Context}. @@ -252,3 +257,31 @@ display_help(#rabbit_cli{progname = Progname, Help = argparse:help(ArgparseDef, Options), io:format("~s~n", [Help]), ok. + +display_version(_Context) -> + case application:get_key(rabbit, vsn) of + {ok, _} -> + ProductInfo = rabbit:product_info(), + ProductName = maps:get( + product_name, ProductInfo, + maps:get(product_base_name, ProductInfo)), + ProductVersion = maps:get( + product_version, ProductInfo, + maps:get(product_base_version, ProductInfo)), + OtpRelease = maps:get(otp_release, ProductInfo), + State = rabbit_boot_state:get(), + io:format( + "~ts ~ts~n" + "Erlang/OTP ~ts~n" + "Status: ~ts~n", + [ProductName, ProductVersion, OtpRelease, State]); + _ -> + OtpRelease = erlang:system_info(otp_release), + ok = application:load(rabbit), + {ok, Vsn} = application:get_key(rabbit, vsn), + io:format( + "RabbitMQ ~ts~n" + "Erlang/OTP ~ts~n" + "Status: (not connected to a RabbitMQ node)~n", + [Vsn, OtpRelease]) + end. From 2e7e8f31d4a4cf19b1f24bb346725b127aadaaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Fri, 27 Jun 2025 22:29:23 +0200 Subject: [PATCH 51/51] Support offline command execution --- deps/rabbit/src/rabbit_cli_backend.erl | 7 +++- deps/rabbit/src/rabbit_cli_commands.erl | 2 +- deps/rabbit/src/rabbit_cli_frontend.erl | 42 ++++++++++++++++-------- deps/rabbit/src/rabbit_cli_transport.erl | 14 ++++++-- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/deps/rabbit/src/rabbit_cli_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl index da69d82df118..bc68dc2eccd2 100644 --- a/deps/rabbit/src/rabbit_cli_backend.erl +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -25,7 +25,12 @@ run_command(ContextMap, Caller) when is_map(ContextMap) -> Context = map_to_context(ContextMap), run_command(Context, Caller); run_command(#rabbit_cli{} = Context, Caller) when is_pid(Caller) -> - rabbit_cli_backend_sup:start_backend(Context, Caller). + try + rabbit_cli_backend_sup:start_backend(Context, Caller) + catch + exit:{noproc, _} -> + start_link(Context, Caller) + end. map_to_context(ContextMap) -> Progname = maps:get(progname, ContextMap), diff --git a/deps/rabbit/src/rabbit_cli_commands.erl b/deps/rabbit/src/rabbit_cli_commands.erl index fc9025774d93..2fe0060b98d1 100644 --- a/deps/rabbit/src/rabbit_cli_commands.erl +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -369,7 +369,7 @@ cmd_noop(_) -> ok. cmd_hello(Context) -> - io:format("Regural prompt test; type Enter to submit~n"), + io:format("Regular prompt test; type Enter to submit~n"), Name = io:get_line("Name: "), case Name of Name when is_list(Name) -> diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl index 06becb1b5e77..e57e861b1c65 100644 --- a/deps/rabbit/src/rabbit_cli_frontend.erl +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -176,15 +176,18 @@ connect_to_node( _ -> rabbit_cli_transport:connect() end, - {ClientInfo, Priv1} = case Ret of - {ok, Connection} -> - {rabbit_cli_transport:get_client_info( - Connection), - Priv#?MODULE{connection = Connection}}; - {error, _Reason} -> - {undefined, - Priv#?MODULE{connection = none}} - end, + Priv1 = case Ret of + {ok, Connection} -> + Priv#?MODULE{connection = Connection}; + {error, Reason} -> + ?LOG_DEBUG( + "CLI: failed to establish a connection to a RabbitMQ " + "node: ~0p", + [Reason]), + Priv#?MODULE{connection = none} + end, + ClientInfo = rabbit_cli_transport:get_client_info( + Priv1#?MODULE.connection), Context1 = Context#rabbit_cli{client = ClientInfo, priv = Priv1}, run_command(Context1). @@ -282,18 +285,31 @@ run_command( main_loop(Context1) end; run_command(#rabbit_cli{} = Context) -> - %% TODO: If we can't connect to a node, try to parse args locally and run - %% the command on this CLI node. %% FIXME: Load applications first, otherwise module attributes are %% unavailable. - %% FIXME: run_command() relies on rabbit_cli_backend_sup. maybe process_flag(trap_exit, true), + prepare_offline_exec(Context), ContextMap = context_to_map(Context), {ok, _Backend} ?= rabbit_cli_backend:run_command(ContextMap, self()), main_loop(Context) end. +prepare_offline_exec(_Context) -> + ?LOG_DEBUG("CLI: prepare for offline execution"), + Env = rabbit_env:get_context(), + rabbit_env:context_to_code_path(Env), + rabbit_env:context_to_app_env_vars(Env), + PluginsDir = rabbit_plugins:plugins_dir(), + Plugins = rabbit_plugins:plugin_names( + rabbit_plugins:list(PluginsDir, true)), + Apps = [rabbit_common, rabbit | Plugins], + lists:foreach( + fun(App) -> _ = application:load(App) end, + Apps), + ?LOG_DEBUG("CLI: ready for offline execution: ~p", [application:loaded_applications()]), + ok. + context_to_map(Context) -> Fields = [Field || Field <- record_info(fields, rabbit_cli), %% We don't need or want to communicate anything that @@ -311,7 +327,7 @@ record_to_map([], _Record, _Index, Map) -> main_loop( #rabbit_cli{priv = #?MODULE{connection = Connection, backend = Backend, - pager = Pager} = Priv} = Context) -> + pager = Pager} = Priv} = Context) -> ?LOG_DEBUG("CLI: frontend main loop (pager: ~0p)...", [Pager]), Timeout = case is_port(Pager) of false -> diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl index 4de9df3032d9..7193dacb6501 100644 --- a/deps/rabbit/src/rabbit_cli_transport.erl +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -90,7 +90,11 @@ complete_nodename(Nodename) -> get_client_info(#?MODULE{type = Proto}) -> {ok, Hostname} = inet:gethostname(), #{hostname => Hostname, - proto => Proto}. + proto => Proto}; +get_client_info(none) -> + {ok, Hostname} = inet:gethostname(), + #{hostname => Hostname, + proto => none}. run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> Caller = self(), @@ -101,9 +105,13 @@ run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> gen_reply(#?MODULE{type = erldist}, From, Reply) -> gen:reply(From, Reply); gen_reply(#?MODULE{type = http, peer = Client}, From, Reply) -> - rabbit_cli_http_client:gen_reply(Client, From, Reply). + rabbit_cli_http_client:gen_reply(Client, From, Reply); +gen_reply(none, From, Reply) -> + gen:reply(From, Reply). send(#?MODULE{type = erldist}, Dest, Msg) -> erlang:send(Dest, Msg); send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> - rabbit_cli_http_client:send(Client, Dest, Msg). + rabbit_cli_http_client:send(Client, Dest, Msg); +send(none, Dest, Msg) -> + erlang:send(Dest, Msg). pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy