diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile
index 0a786304751c..82c45775855c 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
@@ -158,6 +158,28 @@ DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk
include ../../rabbitmq-components.mk
include ../../erlang.mk
+ESCRIPT_NAME := rabbit_cli_frontend
+ESCRIPT_FILE := scripts/rabbitmq
+
+ebin/$(PROJECT).app:: $(ESCRIPT_FILE)
+
+$(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)" > "$@"
+ $(verbose) cat $< >> "$@"
+ $(verbose) chmod a+x "$@"
+
+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/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 @@
+
+
+
+
+ Codestin Search App
+
+
+
+
+ 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 ba20e864fdb3..7bb36a6f9c48 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/priv/schema/rabbitmqctl.schema b/deps/rabbit/priv/schema/rabbitmqctl.schema
new file mode 100644
index 000000000000..1d4f6143f7a0
--- /dev/null
+++ b/deps/rabbit/priv/schema/rabbitmqctl.schema
@@ -0,0 +1 @@
+%% vim:ft=erlang:sw=4:et:
diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl
index 20bd4765b2a3..48ea9af5ddf9 100644
--- a/deps/rabbit/src/rabbit.erl
+++ b/deps/rabbit/src/rabbit.erl
@@ -229,6 +229,26 @@
{requires, [core_initialized, recovery]},
{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,
+ []}},
+ {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, []}},
diff --git a/deps/rabbit/src/rabbit_cli.erl b/deps/rabbit/src/rabbit_cli.erl
new file mode 100644
index 000000000000..f79e2f43db29
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli.erl
@@ -0,0 +1,242 @@
+-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_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl
new file mode 100644
index 000000000000..4ffc76583f37
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_backend.erl
@@ -0,0 +1,121 @@
+-module(rabbit_cli_backend).
+
+-behaviour(gen_statem).
+
+-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([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, 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).
+
+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.
+%% -------------------------------------------------------------------
+
+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.
+
+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).
+
+%% -------------------------------------------------------------------
+%% Command execution.
+%% -------------------------------------------------------------------
+
+do_run_command(
+ #rabbit_cli{command = #{handler := {Module, Function}}} = Context) ->
+ erlang:apply(Module, Function, [Context]).
diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl
new file mode 100644
index 000000000000..e0df9c7077e2
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_backend.hrl
@@ -0,0 +1,8 @@
+-record(rabbit_cli, {progname,
+ args,
+ argparse_def,
+ arg_map,
+ cmd_path,
+ 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
new file mode 100644
index 000000000000..a0967dcda901
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_commands.erl
@@ -0,0 +1,245 @@
+-module(rabbit_cli_commands).
+
+-include_lib("kernel/include/logger.hrl").
+
+-include_lib("rabbit_common/include/logging.hrl").
+-include_lib("rabbit_common/include/resource.hrl").
+
+-export([discover_commands/0,
+ discovered_commands/0,
+ discovered_argparse_def/0]).
+-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]}},
+ #{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}}]}).
+
+-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",
+ handler => {?MODULE, cmd_top}}]}).
+
+%% -------------------------------------------------------------------
+%% Commands discovery.
+%% -------------------------------------------------------------------
+
+discover_commands() ->
+ _ = discovered_commands_and_argparse_def(),
+ ok.
+
+discovered_commands_and_argparse_def() ->
+ Key = {?MODULE, discovered_commands},
+ try
+ persistent_term:get(Key)
+ catch
+ error:badarg ->
+ Commands = do_discover_commands(),
+ ArgparseDef = commands_to_cli_argparse_def(Commands),
+ Cache = #{commands => Commands,
+ argparse_def => ArgparseDef},
+ persistent_term:put(Key, Cache),
+ Cache
+ end.
+
+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(),
+ 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).
+
+%% -------------------------------------------------------------------
+%% 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],
+ 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.
+ 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).
+
+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_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl
new file mode 100644
index 000000000000..babd9a7c889d
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_frontend.erl
@@ -0,0 +1,232 @@
+-module(rabbit_cli_frontend).
+
+-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,
+ connection}).
+
+main(Args) ->
+ ScriptName = escript:script_name(),
+ add_rabbitmq_code_path(ScriptName),
+ configure_logging(),
+
+ 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().
+
+%% -------------------------------------------------------------------
+%% CLI frontend setup.
+%% -------------------------------------------------------------------
+
+add_rabbitmq_code_path(ScriptName) ->
+ ScriptDir = filename:dirname(ScriptName),
+ 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).
+
+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"),
+ 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#rabbit_cli{argparse_def = LocalArgparseDef},
+
+ {ok,
+ PartialArgMap,
+ PartialCmdPath,
+ PartialCommand} ?= initial_parse(Context1),
+ Context2 = Context1#rabbit_cli{arg_map = PartialArgMap,
+ cmd_path = PartialCmdPath,
+ command = PartialCommand},
+ set_log_level(Context2)
+ end.
+
+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(#rabbit_cli{} = Context) ->
+ connect_to_node(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,
+ 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).
+
+%% -------------------------------------------------------------------
+%% Arguments definition and parsing.
+%% -------------------------------------------------------------------
+
+initial_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(
+ #rabbit_cli{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.
+
+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
+%% * 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(
+ #rabbit_cli{frontend_priv = #?MODULE{connection = Connection}} = Context)
+ when Connection =/= none ->
+ 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: 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, 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) ->
+ Value = element(Index, Record),
+ Map1 = Map#{Field => Value},
+ record_to_map(Rest, Record, Index + 1, Map1);
+record_to_map([], _Record, _Index, Map) ->
+ Map.
+
+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
new file mode 100644
index 000000000000..5c10e22ee589
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_http_client.erl
@@ -0,0 +1,199 @@
+-module(rabbit_cli_http_client).
+
+-behaviour(gen_statem).
+
+-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,
+ 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, []).
+
+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}).
+
+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 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};
+ {stop, Reason} ->
+ {stop, Reason, Data}
+ 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) ->
+ ?LOG_DEBUG("CLI: HTTP client terminating: ~0p", [Reason]),
+ 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};
+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
new file mode 100644
index 000000000000..96818c2b399f
--- /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) ->
+ 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) 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}.
+
+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]),
+ 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).
+
+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_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_io.erl b/deps/rabbit/src/rabbit_cli_io.erl
new file mode 100644
index 000000000000..4104963f78f3
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_io.erl
@@ -0,0 +1,344 @@
+-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/1,
+ format/3,
+ 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,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-record(?MODULE, {progname,
+ record_streams = #{},
+ 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) ->
+ #{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 => input,
+ long => "-input",
+ short => $i,
+ type => string,
+ nargs => 1,
+ help => "Read input from file "}
+ ]
+ }.
+
+display_help(#{io := {transport, Transport}} = Context) ->
+ Transport ! {io_cast, {?FUNCTION_NAME, 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;
+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}).
+
+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;
+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},
+ {ok, State}.
+
+handle_call(
+ {start_record_stream, Name, Fields, ArgMap},
+ From,
+ #?MODULE{record_streams = Streams} = State) ->
+ Stream = #{name => Name, fields => Fields, arg_map => ArgMap},
+ Streams1 = Streams#{Name => Stream},
+ State1 = State#?MODULE{record_streams = Streams1},
+ gen_server:reply(From, {ok, Stream}),
+
+ {ok, State2} = format_record_stream_start(Name, State1),
+
+ {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, compute_timeout(State1)};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State, compute_timeout(State)}.
+
+handle_cast(
+ {display_help, #{cmd_path := CmdPath, argparse_def := 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, compute_timeout(State)};
+handle_cast({format, Format, Args}, State) ->
+ io:format(Format, Args),
+ {noreply, State, compute_timeout(State)};
+handle_cast({push_new_record, Name, Record}, State) ->
+ {ok, State1} = format_record(Name, Record, State),
+ {noreply, State1, compute_timeout(State1)};
+handle_cast({end_record_stream, Name}, State) ->
+ {ok, State1} = format_record_stream_end(Name, State),
+ {noreply, State1, compute_timeout(State1)};
+handle_cast(_Request, 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, compute_timeout(State)}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+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) ->
+ 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, []).
+
+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],
+ 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 := 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.
+
+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.
diff --git a/deps/rabbit/src/rabbit_cli_transport.erl b/deps/rabbit/src/rabbit_cli_transport.erl
new file mode 100644
index 000000000000..552fd2472283
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_transport.erl
@@ -0,0 +1,216 @@
+-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]).
+
+-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()
+ }).
+
+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).
+
+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).
+
+%% -------------------------------------------------------------------
+%% Erlang distribution.
+%% -------------------------------------------------------------------
+
+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) ->
+ 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_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).
diff --git a/deps/rabbit/src/rabbit_cli_transport2.erl b/deps/rabbit/src/rabbit_cli_transport2.erl
new file mode 100644
index 000000000000..50f731009d87
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_transport2.erl
@@ -0,0 +1,109 @@
+-module(rabbit_cli_transport2).
+
+-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,
+ 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.
+
+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 = 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).
diff --git a/deps/rabbit/src/rabbit_cli_ws.erl b/deps/rabbit/src/rabbit_cli_ws.erl
new file mode 100644
index 000000000000..e103bc60d1a5
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_ws.erl
@@ -0,0 +1,114 @@
+-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
new file mode 100644
index 000000000000..d5d38cc04711
--- /dev/null
+++ b/deps/rabbit/src/rabbit_cli_ws_runner.erl
@@ -0,0 +1,62 @@
+-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}.
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
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)).