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)).