diff --git a/deps/rabbit/.gitignore b/deps/rabbit/.gitignore index 9e124a080135..0c704442d2aa 100644 --- a/deps/rabbit/.gitignore +++ b/deps/rabbit/.gitignore @@ -4,3 +4,6 @@ [Bb]in/ [Oo]bj/ + +/scripts/rabbitmq +/scripts/rabbitmq.escript diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index 5153918dd56f..466517503ff5 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -129,7 +129,7 @@ endef LOCAL_DEPS = sasl os_mon inets compiler public_key crypto ssl syntax_tools xmerl BUILD_DEPS = rabbitmq_cli -DEPS = ranch cowlib rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat horus khepri khepri_mnesia_migration cuttlefish gen_batch_server +DEPS = ranch cowlib rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat horus khepri khepri_mnesia_migration cuttlefish gen_batch_server cowboy gun eterminfo TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_client rabbitmq_amqp_client rabbitmq_amqp1_0 # We pin a version of Horus even if we don't use it directly (it is a @@ -138,6 +138,8 @@ TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_clie # should be removed with the next update of Khepri. dep_horus = hex 0.3.1 +dep_eterminfo = git https://github.com/tomas-abrahamsson/eterminfo.git 2.0 + PLT_APPS += mnesia runtime_tools dep_syslog = git https://github.com/schlagert/syslog 4.0.0 @@ -158,6 +160,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 @@ + + + + + RabbitMQ CLI over HTTP + + + + +

RabbitMQ CLI over HTTP

+
+
+ + + +
+
+

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

+

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

+
rabbitmqctl \
+    --node <URL> \
+    <command>
+
+
+ + diff --git a/deps/rabbit/priv/schema/rabbit.schema b/deps/rabbit/priv/schema/rabbit.schema index f5b79370fcd6..b89997349cbe 100644 --- a/deps/rabbit/priv/schema/rabbit.schema +++ b/deps/rabbit/priv/schema/rabbit.schema @@ -112,6 +112,14 @@ end}. {datatype, {enum, [true, false]}} ]}. +{mapping, "listeners.cli.$proto", "rabbit.cli_listeners",[ + {datatype, [integer, ip]} +]}. +{translation, "rabbit.cli_listeners", +fun(Conf) -> + cuttlefish_variable:filter_by_prefix("listeners.cli", Conf) +end}. + {mapping, "erlang.K", "vm_args.+K", [ {default, "true"}, {level, advanced} diff --git a/deps/rabbit/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/scripts/rabbitmq-diagnostics b/deps/rabbit/scripts/rabbitmq-diagnostics deleted file mode 100755 index c874df8cdf3f..000000000000 --- a/deps/rabbit/scripts/rabbitmq-diagnostics +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -maybe_noinput='noinput' - -case "$@" in - *observer*) - maybe_noinput='input' - ;; - *remote_shell*) - maybe_noinput='input' - ;; - *) - maybe_noinput='noinput' - ;; -esac - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-diagnostics "$maybe_noinput" "$@" diff --git a/deps/rabbit/scripts/rabbitmq-diagnostics.bat b/deps/rabbit/scripts/rabbitmq-diagnostics.bat deleted file mode 100644 index bb29099d14da..000000000000 --- a/deps/rabbit/scripts/rabbitmq-diagnostics.bat +++ /dev/null @@ -1,62 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -if "%1"=="remote_shell" ( - set ERL_CMD=werl.exe -) else ( - set ERL_CMD=erl.exe -) - -REM Note: do NOT add -noinput because "observer" depends on it -"!ERLANG_HOME!\bin\!ERL_CMD!" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-diagnostics" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-plugins b/deps/rabbit/scripts/rabbitmq-plugins deleted file mode 100755 index 3e1ea1d3360b..000000000000 --- a/deps/rabbit/scripts/rabbitmq-plugins +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-plugins 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-plugins.bat b/deps/rabbit/scripts/rabbitmq-plugins.bat deleted file mode 100644 index 553ba7a0b558..000000000000 --- a/deps/rabbit/scripts/rabbitmq-plugins.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off - -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "!TDP0!\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-plugins" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-queues b/deps/rabbit/scripts/rabbitmq-queues deleted file mode 100755 index 79e9cef052af..000000000000 --- a/deps/rabbit/scripts/rabbitmq-queues +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-queues 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-queues.bat b/deps/rabbit/scripts/rabbitmq-queues.bat deleted file mode 100644 index b38a1332fbf6..000000000000 --- a/deps/rabbit/scripts/rabbitmq-queues.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-queues" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-streams b/deps/rabbit/scripts/rabbitmq-streams deleted file mode 100755 index b04e23ba9dbf..000000000000 --- a/deps/rabbit/scripts/rabbitmq-streams +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-streams 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-streams.bat b/deps/rabbit/scripts/rabbitmq-streams.bat deleted file mode 100644 index e34359cea4a2..000000000000 --- a/deps/rabbit/scripts/rabbitmq-streams.bat +++ /dev/null @@ -1,55 +0,0 @@ -@echo off - -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-streams" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/scripts/rabbitmq-upgrade b/deps/rabbit/scripts/rabbitmq-upgrade deleted file mode 100755 index 7a067e3bd7b5..000000000000 --- a/deps/rabbit/scripts/rabbitmq-upgrade +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmq-upgrade 'noinput' "$@" diff --git a/deps/rabbit/scripts/rabbitmq-upgrade.bat b/deps/rabbit/scripts/rabbitmq-upgrade.bat deleted file mode 100644 index d0229f7a581f..000000000000 --- a/deps/rabbit/scripts/rabbitmq-upgrade.bat +++ /dev/null @@ -1,55 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmq-upgrade" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal diff --git a/deps/rabbit/scripts/rabbitmqctl b/deps/rabbit/scripts/rabbitmqctl deleted file mode 100755 index 2a3dac189c59..000000000000 --- a/deps/rabbit/scripts/rabbitmqctl +++ /dev/null @@ -1,164 +0,0 @@ -#!/bin/sh -## This Source Code Form is subject to the terms of the Mozilla Public -## License, v. 2.0. If a copy of the MPL was not distributed with this -## file, You can obtain one at https://mozilla.org/MPL/2.0/. -## -## Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -## - -# Exit immediately if a pipeline, which may consist of a single simple command, -# a list, or a compound command returns a non-zero status -set -e - -# Each variable or function that is created or modified is given the export -# attribute and marked for export to the environment of subsequent commands. -set -a - -# shellcheck source=/dev/null -# -# TODO: when shellcheck adds support for relative paths, change to -# shellcheck source=./rabbitmq-env -. "${0%/*}"/rabbitmq-env - -# Uncomment for debugging -# echo "\$# : $#" -# echo "\$0: $0" -# echo "\$1: $1" -# echo "\$2: $2" -# echo "\$3: $3" -# echo "\$4: $4" -# echo "\$5: $5" -# set -x - -_tmp_help_requested='false' - -for _tmp_argument in "$@" -do - if [ "$_tmp_argument" = '--help' ] - then - _tmp_help_requested='true' - break - fi -done - -if [ "$1" = 'help' ] || [ "$_tmp_help_requested" = 'true' ] -then - unset _tmp_help_requested - # In this case, we do not require input and can exit early since - # help was requested - # - run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmqctl 'noinput' "$@" - exit "$?" -fi - -unset _tmp_help_requested - -maybe_noinput='noinput' - -case "$@" in - *add_user*) - if [ "$#" -eq 2 ] - then - # In this case, input is required to provide the password: - # - # rabbitmqctl add_user bob - # - maybe_noinput='input' - elif [ "$#" -eq 3 ] - then - # In these cases, input depends on the arguments provided: - # - # rabbitmqctl add_user bob --pre-hashed-password (input needed) - # rabbitmqctl add_user bob bobpassword (NO input needed) - # rabbitmqctl add_user --pre-hashed-password bob (input needed) - # - for _tmp_argument in "$@" - do - if [ "$_tmp_argument" = '--pre-hashed-password' ] - then - maybe_noinput='input' - break - fi - done - elif [ "$#" -gt 3 ] - then - # If there are 4 or more arguments, no input is needed: - # - # rabbitmqctl add_user bob --pre-hashed-password HASHVALUE - # rabbitmqctl add_user bob bobpassword IGNORED - # rabbitmqctl add_user --pre-hashed-password bob HASHVALUE - # - maybe_noinput='noinput' - fi - ;; - *authenticate_user*) - if [ "$#" -eq 2 ] - then - # In this case, input is required to provide the password: - # - # rabbitmqctl authenticate_user bob - # - maybe_noinput='input' - elif [ "$#" -gt 2 ] - then - # If there are 2 or more arguments, no input is needed: - # - maybe_noinput='noinput' - fi - ;; - *change_password*) - maybe_noinput='input' - if [ "$#" -gt 2 ] - then - # If there are 3 or more arguments, no input is needed: - # - # rabbitmqctl change_password sue foobar - # rabbitmqctl change_password sue newpassword IGNORED - # - maybe_noinput='noinput' - fi - ;; - *decode*|*encode*) - # It is unlikely that these commands will be run in a shell script loop - # with redirection, so always assume that stdin input is needed - # - maybe_noinput='input' - ;; - *eval*) - if [ "$#" -eq 1 ] - then - # If there is only one argument, 'eval', then input is required - # - # rabbitmqctl eval - # - maybe_noinput='input' - fi - ;; - *hash_password*) - if [ "$#" -eq 1 ] - then - # If there is only one argument, 'hash_password', then input is required - # - # rabbitmqctl hash_password - # - maybe_noinput='input' - fi - ;; - *import_definitions*) - if [ "$#" -eq 1 ] - then - # If there is only one argument, 'import_definitions', then input is required - # - # rabbitmqctl import_definitions - # - maybe_noinput='input' - fi - ;; - *) - maybe_noinput='noinput' - ;; -esac - -unset _tmp_argument - -run_escript "${ESCRIPT_DIR:?must be defined}"/rabbitmqctl "$maybe_noinput" "$@" diff --git a/deps/rabbit/scripts/rabbitmqctl.bat b/deps/rabbit/scripts/rabbitmqctl.bat deleted file mode 100644 index 9afe78c6f1bc..000000000000 --- a/deps/rabbit/scripts/rabbitmqctl.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -REM This Source Code Form is subject to the terms of the Mozilla Public -REM License, v. 2.0. If a copy of the MPL was not distributed with this -REM file, You can obtain one at https://mozilla.org/MPL/2.0/. -REM -REM Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. -REM - -REM Scopes the variables to the current batch file -setlocal - -rem Preserve values that might contain exclamation marks before -rem enabling delayed expansion -set TDP0=%~dp0 -set STAR=%* -setlocal enabledelayedexpansion - -REM Get default settings with user overrides for (RABBITMQ_) -REM Non-empty defaults should be set in rabbitmq-env -call "%TDP0%\rabbitmq-env.bat" %~n0 - -if not exist "!ERLANG_HOME!\bin\erl.exe" ( - echo. - echo ****************************** - echo ERLANG_HOME not set correctly. - echo ****************************** - echo. - echo Please either set ERLANG_HOME to point to your Erlang installation or place the - echo RabbitMQ server distribution in the Erlang lib folder. - echo. - exit /B 1 -) - -REM Disable erl_crash.dump by default for control scripts. -if not defined ERL_CRASH_DUMP_SECONDS ( - set ERL_CRASH_DUMP_SECONDS=0 -) - -"!ERLANG_HOME!\bin\erl.exe" +B ^ --boot !CLEAN_BOOT_FILE! ^ --noinput -noshell -hidden -smp enable ^ -!RABBITMQ_CTL_ERL_ARGS! ^ --kernel inet_dist_listen_min !RABBITMQ_CTL_DIST_PORT_MIN! ^ --kernel inet_dist_listen_max !RABBITMQ_CTL_DIST_PORT_MAX! ^ --run escript start ^ --escript main Elixir.RabbitMQCtl ^ --extra "%RABBITMQ_HOME%\escript\rabbitmqctl" !STAR! - -if ERRORLEVEL 1 ( - exit /B %ERRORLEVEL% -) - -EXIT /B 0 - -endlocal -endlocal diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index 3657f60f05bd..50df0d421ed4 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -235,6 +235,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_server, + [{description, "RabbitMQ CLI HTTP listener"}, + {mfa, {rabbit_sup, start_restartable_child, + [rabbit_cli_http_server]}}, + {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_backend.erl b/deps/rabbit/src/rabbit_cli_backend.erl new file mode 100644 index 000000000000..bc68dc2eccd2 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_backend.erl @@ -0,0 +1,292 @@ +-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, + send_frontend_request/2, + start_link/2, + i/0]). +-export([init/1, + callback_mode/0, + handle_event/4, + terminate/3, + code_change/4]). + +-record(?MODULE, {caller}). + +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) -> + try + rabbit_cli_backend_sup:start_backend(Context, Caller) + catch + exit:{noproc, _} -> + start_link(Context, Caller) + end. + +map_to_context(ContextMap) -> + Progname = maps:get(progname, ContextMap), + Legacy = is_legacy_progname(Progname), + #rabbit_cli{progname = Progname, + args = maps:get(args, ContextMap), + argparse_def = maps:get(argparse_def, ContextMap), + arg_map = maps:get(arg_map, ContextMap), + cmd_path = maps:get(cmd_path, ContextMap), + command = maps:get(command, ContextMap), + legacy = Legacy, + os = maps:get(os, ContextMap), + client = maps:get(client, ContextMap), + env = maps:get(env, ContextMap), + terminal = maps:get(terminal, ContextMap)}. + +is_legacy_progname("rabbitmqctl") -> + true; +is_legacy_progname("rabbitmq-diagnostics") -> + true; +is_legacy_progname("rabbitmq-plugins") -> + true; +is_legacy_progname("rabbitmq-queues") -> + true; +is_legacy_progname("rabbitmq-streams") -> + true; +is_legacy_progname("rabbitmq-upgrade") -> + true; +is_legacy_progname(_Progname) -> + false. + +start_link(Context, Caller) -> + Args = #{context => Context, + caller => Caller}, + gen_statem:start_link(?MODULE, Args, []). + +send_frontend_request( + #rabbit_cli{priv = #?MODULE{caller = Caller}}, Request) -> + Mref = erlang:monitor(process, Caller), + Caller ! {frontend_request, {self(), Mref}, Request}, + receive + {Mref, Reply} -> + erlang:demonitor(Mref, [flush]), + Reply; + {'DOWN', Mref, _, _, Reason} -> + exit(Reason) + end. + +i() -> + Backends = rabbit_cli_backend_sup:which_backends(), + i(Backends). + +i([]) -> + io:format("No CLI commands running~n"); +i(Backends) -> + io:format("Running commands:~n"), + Now = erlang:system_time(), + lists:foreach( + fun(Backend) -> + #{cmdline := CmdLine, + client := #{hostname := Hostname, proto := Proto}, + started_at := StartTime} = proc_lib:get_label(Backend), + Duration = erlang:convert_time_unit( + Now - StartTime, native, seconds), + FormattedCmdLine = format_cmdline(CmdLine), + FormattedProto = case Proto of + erldist -> + "Erlang distribution"; + http -> + "HTTP"; + _ -> + Proto + end, + io:format( + " - ~ts~n (running for ~b seconds, started from \"~ts\", " + "protocol: ~ts)~n", + [FormattedCmdLine, Duration, Hostname, FormattedProto]) + end, Backends). + +%% ------------------------------------------------------------------- +%% gen_statem callbacks. +%% ------------------------------------------------------------------- + +init( + #{context := #rabbit_cli{progname = Progname, + args = Args, + client = #{hostname := Hostname, + proto := Proto} = ClientInfo, + terminal = Terminal} = Context, + caller := Caller + }) -> + %% Do not trap EXIT signal. This ensures the command is stopped. Because + %% it could be running a blocking call or receive and the EXIT signal + %% could seat in its inbox for a long time. + + erlang:link(Caller), + erlang:group_leader(Caller, self()), + + CmdLine = [Progname | Args], + StartTime = erlang:system_time(), + Label = #{cmdline => CmdLine, + client => ClientInfo, + started_at => StartTime}, + proc_lib:set_label(Label), + ?LOG_INFO( + "CLI: running: ~ts (started from \"~ts\", protocol: ~ts)", + [format_cmdline(CmdLine), Hostname, Proto]), + ?LOG_DEBUG( + "CLI: tty: stdout=~s stderr=~s stdin=~s", + [maps:get(stdout, Terminal), + maps:get(stderr, Terminal), + maps:get(stdin, Terminal)]), + + Priv = #?MODULE{caller = Caller}, + Context1 = Context#rabbit_cli{priv = Priv}, + {ok, standing_by, Context1, {next_event, internal, parse_command}}. + +format_cmdline(CmdLine) -> + FormattedArgs = [begin + case string:chr(Arg, $') of + 0 -> + Arg; + _ -> + Arg1 = re:replace(Arg, "'", "\\'", [global]), + "'" ++ Arg1 ++ "'" + end + end || Arg <- CmdLine], + string:join(FormattedArgs, " "). + +callback_mode() -> + handle_event_function. + +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, + #rabbit_cli{arg_map = #{help := true}} = Context) -> + display_help(Context), + {stop, {shutdown, ok}, Context}; +handle_event( + internal, run_command, command_parsed, + #rabbit_cli{arg_map = #{version := true}} = Context) -> + display_version(Context), + {stop, {shutdown, ok}, Context}; +handle_event(internal, run_command, command_parsed, Context) -> + Ret = do_run_command(Context), + {stop, {shutdown, Ret}, Context}. + +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} = Context) -> + FullArgparseDef = rabbit_cli_commands:discovered_argparse_def(), + ArgparseDef1 = rabbit_cli_commands:merge_argparse_def( + PartialArgparseDef, FullArgparseDef), + ArgparseDef2 = filter_legacy_commands(Context, ArgparseDef1), + ArgparseDef2. + +filter_legacy_commands(#rabbit_cli{legacy = Legacy} = Context, ArgparseDef) -> + case ArgparseDef of + #{commands := Commands} -> + Commands1 = ( + maps:filtermap( + fun(_CmdName, Command) -> + Keep = (Legacy =:= is_legacy_command(Command)), + case Keep of + true -> + Command1 = filter_legacy_commands( + Context, Command), + {true, Command1}; + false -> + false + end + end, Commands)), + ArgparseDef#{commands => Commands1}; + _ -> + ArgparseDef + end. + +is_legacy_command(#{legacy := true}) -> + true; +is_legacy_command(_Command) -> + false. + +final_parse( + #rabbit_cli{progname = ProgName, args = Args, argparse_def = ArgparseDef}) -> + 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]). + +display_help(#rabbit_cli{progname = Progname, + argparse_def = ArgparseDef, + arg_map = #{help := true}, + cmd_path = CmdPath}) -> + Options = #{progname => Progname, + %% Work around bug in argparse; + %% See https://github.com/erlang/otp/pull/9160 + command => tl(CmdPath)}, + Help = argparse:help(ArgparseDef, Options), + io:format("~s~n", [Help]), + ok. + +display_version(_Context) -> + case application:get_key(rabbit, vsn) of + {ok, _} -> + ProductInfo = rabbit:product_info(), + ProductName = maps:get( + product_name, ProductInfo, + maps:get(product_base_name, ProductInfo)), + ProductVersion = maps:get( + product_version, ProductInfo, + maps:get(product_base_version, ProductInfo)), + OtpRelease = maps:get(otp_release, ProductInfo), + State = rabbit_boot_state:get(), + io:format( + "~ts ~ts~n" + "Erlang/OTP ~ts~n" + "Status: ~ts~n", + [ProductName, ProductVersion, OtpRelease, State]); + _ -> + OtpRelease = erlang:system_info(otp_release), + ok = application:load(rabbit), + {ok, Vsn} = application:get_key(rabbit, vsn), + io:format( + "RabbitMQ ~ts~n" + "Erlang/OTP ~ts~n" + "Status: (not connected to a RabbitMQ node)~n", + [Vsn, OtpRelease]) + end. diff --git a/deps/rabbit/src/rabbit_cli_backend.hrl b/deps/rabbit/src/rabbit_cli_backend.hrl new file mode 100644 index 000000000000..975b9ccc225d --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_backend.hrl @@ -0,0 +1,20 @@ +-record(rabbit_cli, {progname :: string(), + args :: argparse:args(), + argparse_def :: argparse:command() | undefined, + arg_map :: argparse:arg_map() | undefined, + cmd_path :: argparse:cmd_path() | undefined, + command :: argparse:command() | undefined, + legacy :: boolean() | undefined, + + os :: {unix | win32, atom()}, + client :: #{hostname := string(), + proto := atom()} | undefined, + env :: [{os:env_var_name(), os:env_var_value()}], + terminal :: #{stdout := boolean(), + stderr := boolean(), + stdin := boolean(), + + name := eterminfo:term_name(), + info := eterminfo:terminfo()}, + + 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..dbb9a61aed1a --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_backend_sup.erl @@ -0,0 +1,25 @@ +-module(rabbit_cli_backend_sup). + +-behaviour(supervisor). + +-export([start_link/0, + start_backend/2, + which_backends/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, none). + +start_backend(Context, Caller) -> + supervisor:start_child(?MODULE, [Context, Caller]). + +which_backends() -> + Children = supervisor:which_children(?MODULE), + [Child || {_ChildId, Child, _Type, _Modules} <- Children]. + +init(_Args) -> + SupFlags = #{strategy => simple_one_for_one}, + BackendChild = #{id => rabbit_cli_backend, + 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..2fe0060b98d1 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_commands.erl @@ -0,0 +1,529 @@ +-module(rabbit_cli_commands). + +-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([discover_commands/0, + discovered_commands/0, + discovered_argparse_def/0, + merge_argparse_def/2]). +-export([cmd_generate_completion_script/1, + cmd_noop/1, + cmd_hello/1, + cmd_pager/1, + cmd_signal/1, + cmd_crash/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 => ["page"]}, + #{help => "Test pager", + handler => {?MODULE, cmd_pager}}}). + +-rabbitmq_command( + {#{cli => ["signal"]}, + #{help => "Test signal handling", + handler => {?MODULE, cmd_signal}}}). + +-rabbitmq_command( + {#{cli => ["crash"]}, + #{help => "Crash", + 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 => ["top"]}, + [#{help => "Top-like interactive view", + handler => {?MODULE, cmd_top}}]}). + +-rabbitmq_command( + {#{cli => ["generate", "completion"]}, + #{help => "Generate a completion script for the given shell", + arguments => [ + #{name => shell, + type => {binary, [<<"fish">>]}, + required => true, + help => "Name of the shell to target"} + ], + handler => {?MODULE, cmd_generate_completion_script}}}). + +%% ------------------------------------------------------------------- +%% Commands discovery. +%% ------------------------------------------------------------------- + +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: 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), + merge_argparse_def(Acc1, M1); + (_, Acc1) -> + Acc1 + end, Acc0, Entries) + end, #{}, Commands). + +%% ------------------------------------------------------------------- +%% Argparse helpers. +%% ------------------------------------------------------------------- + +expand_argparse_def(Def) when is_map(Def) -> + Def; +expand_argparse_def(Defs) when is_list(Defs) -> + lists:foldl( + fun + (Mod, Acc) when is_atom(Mod) -> + Def = Mod:argparse_def(), + merge_argparse_def(Acc, Def); + ({Mod, Function, Args}, Acc) when is_atom(Mod) -> + Def = erlang:apply(Mod, Function, Args), + merge_argparse_def(Acc, Def); + (Def, Acc) when is_map(Def) -> + Def1 = expand_argparse_def(Def), + merge_argparse_def(Acc, Def1) + end, #{}, Defs). + +merge_argparse_def(ArgparseDef1, ArgparseDef2) + when is_map(ArgparseDef1) andalso is_map(ArgparseDef2) -> + Args1 = maps:get(arguments, ArgparseDef1, []), + Args2 = maps:get(arguments, ArgparseDef2, []), + Args = merge_arguments(Args1, Args2), + Cmds1 = maps:get(commands, ArgparseDef1, #{}), + Cmds2 = maps:get(commands, ArgparseDef2, #{}), + Cmds = merge_commands(Cmds1, Cmds2), + maps:merge( + ArgparseDef1, + ArgparseDef2#{arguments => Args, commands => Cmds}). + +merge_arguments(Args1, []) -> + Args1; +merge_arguments([], Args2) -> + Args2; +merge_arguments(Args1, Args2) -> + merge_arguments(Args1, Args2, []). + +merge_arguments([#{name := Name} = Arg1 | Rest1], Args2, Acc) -> + Ret = lists:partition( + fun(#{name := N}) -> N =:= Name end, + Args2), + {Arg, Rest2} = case Ret of + {[Arg2], NotMatching} -> + {Arg2, NotMatching}; + {[], Args2} -> + {Arg1, Args2} + end, + Acc1 = [Arg | Acc], + merge_arguments(Rest1, Rest2, Acc1); +merge_arguments([], Args2, Acc) -> + lists:reverse(Acc) ++ Args2. + +merge_commands(Cmds1, Cmds2) -> + maps:merge(Cmds1, Cmds2). + +%% ------------------------------------------------------------------- +%% Completion files. +%% ------------------------------------------------------------------- + +cmd_generate_completion_script( + #rabbit_cli{arg_map = #{shell := Shell}} = Context) -> + ?LOG_DEBUG("Generating completion script for shell `~ts`", [Shell]), + generate_completion_script(Context, Shell). + +generate_completion_script( + #rabbit_cli{progname = Progname, argparse_def = ArgparseDef} = Context, + <<"fish">>) -> + Chunk1 = io_lib:format( + """ + # Clear any existing completion rules. + complete -c ~ts -e + + # Disable filename completion. + complete -c ~ts -f + + """, [Progname, Progname]), + + Chunk2 = completion_for_fish(Context, ArgparseDef, []), + + io:format("~ts~n~ts", [Chunk1, Chunk2]). + +completion_for_fish(Context, ArgparseDef, CmdPath) -> + Chunk1 = format_arguments_for_fish(Context, ArgparseDef, CmdPath), + Chunk2 = format_commands_for_fish(Context, ArgparseDef, CmdPath), + [Chunk1, Chunk2]. + +format_arguments_for_fish( + #rabbit_cli{progname = Progname}, + #{arguments := Arguments}, + CmdPath) when Arguments =/= [] -> + Chunk = lists:map( + fun(Arg) -> + Cond = case CmdPath of + [] -> + ""; + _ -> + format_cmdpath_cond_for_fish( + CmdPath, []) + end, + Option = format_arg_for_fish(Arg), + Desc = format_desc_for_fish(Arg), + io_lib:format( + "complete -c ~ts~ts~ts~ts~n", + [Progname, Cond, Option, Desc]) + end, Arguments), + [io_lib:nl(), Chunk]; +format_arguments_for_fish(_Context, _ArgparseDef, _CmdPath) -> + "". + +format_commands_for_fish( + #rabbit_cli{progname = Progname} = Context, + #{commands := Commands}, + CmdPath) when Commands =/= #{} -> + CmdNames = lists:sort(maps:keys(Commands)), + Chunk1 = lists:map( + fun(CmdName) -> + Command = maps:get(CmdName, Commands), + Cond = format_cmdpath_cond_for_fish(CmdPath, CmdNames), + Desc = format_desc_for_fish(Command), + io_lib:format( + "complete -c ~ts~ts -a ~ts~ts~n", + [Progname, Cond, CmdName, Desc]) + end, CmdNames), + Chunk2 = lists:map( + fun(CmdName) -> + Command = maps:get(CmdName, Commands), + completion_for_fish( + Context, Command, CmdPath ++ [CmdName]) + end, CmdNames), + [io_lib:nl(), Chunk1, Chunk2]; +format_commands_for_fish(_Context, _ArgparseDef, _CmdPath) -> + "". + +format_cmdpath_cond_for_fish([], _CmdNames) -> + " -n __fish_use_subcommand"; +format_cmdpath_cond_for_fish(CmdPath, CmdNames) -> + CondA = lists:map( + fun(CmdName) -> + io_lib:format( + "__fish_seen_subcommand_from ~ts", + [CmdName]) + end, CmdPath), + CondB = case CmdNames of + [] -> + []; + _ -> + CondB0 = lists:map( + fun(CmdName) -> + io_lib:format("~ts", [CmdName]) + end, CmdNames), + CondB1 = string:join(CondB0, " "), + CondB2 = io_lib:format( + "not __fish_seen_subcommand_from ~ts", + [CondB1]), + [CondB2] + end, + Cond1 = string:join(CondA ++ CondB, " && "), + Cond2 = lists:flatten(Cond1), + io_lib:format(" -n ~0p", [Cond2]). + +format_arg_for_fish(Arg) -> + Long = case Arg of + #{long := [$- | Name]} -> + io_lib:format(" -l ~ts", [Name]); + #{long := Name} -> + io_lib:format(" -o ~ts", [Name]); + _ -> + "" + end, + Short = case Arg of + #{short := Char} -> + io_lib:format(" -s ~tc", [Char]); + _ -> + "" + end, + IsRequired = case Arg of + #{required := R} -> + R; + #{long := _} -> + false; + #{short := _} -> + false; + _ -> + true + end, + Required = case IsRequired of + true -> + " -r"; + false -> + "" + end, + Type = maps:get(type, Arg, string), + ArgArg = case Type of + {ErlType, [H | _] = Choices} + when ErlType =:= atom orelse + ErlType =:= binary orelse + (ErlType =:= string andalso is_list(H)) -> + AA0 = lists:map( + fun(Choice) -> + io_lib:format("~ts", [Choice]) + end, Choices), + AA1 = string:join(AA0, " "), + AA2 = lists:flatten(AA1), + AA3 = io_lib:format(" -a ~p", [AA2]), + AA3; + _ -> + "" + end, + [Long, Short, Required, ArgArg]. + +format_desc_for_fish(#{help := Help}) -> + io_lib:format(" -d ~0p", [Help]); +format_desc_for_fish(_) -> + "". + +%% ------------------------------------------------------------------- +%% XXX +%% ------------------------------------------------------------------- + +cmd_noop(_) -> + ok. + +cmd_hello(Context) -> + io:format("Regular prompt test; type Enter to submit~n"), + Name = io:get_line("Name: "), + case Name of + Name when is_list(Name) -> + io:format("Hello ~s!~n", [string:trim(Name)]), + + io:format( + "~nInteraction mode test; " + "type any arrow keys, click with your mouse, or any other key to quit~n"), + ok = rabbit_cli_io:set_interactive_mode(Context), + io:format("\e[?1003h\e[?1015h\e[?1006h"), + Ret = cmd_hello_loop(Context), + io:format("\e[?1000l"), + Ret; + {error, _} = Error -> + Error + end. + +cmd_hello_loop(Context) -> + case io:get_chars("", 1024) of + Chars when is_list(Chars) -> + case handle_hello_chars(Chars, Context) of + ok -> + cmd_hello_loop(Context); + stop -> + ok + end; + eof -> + ok; + {error, _} = Error -> + Error + end. + +handle_hello_chars("\e[A" ++ Rest, Context) -> + io:format("Key: Up\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[B" ++ Rest, Context) -> + io:format("Key: Down\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[C" ++ Rest, Context) -> + io:format("Key: Right\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[D" ++ Rest, Context) -> + io:format("Key: Left\r~n"), + handle_hello_chars(Rest, Context); +handle_hello_chars("\e[<" ++ Rest, Context) -> + %% Mouse interaction. + {Mouse, Rest1} = split_escape_sequence(Rest, ""), + Attrs = string:lexemes(Mouse, ";"), + case Attrs of + ["35", Col0, Line0] -> + {Line, _} = string:to_integer(Line0), + {Col, ""} = string:to_integer(Col0), + io:format("Mouse: Move, line ~b, column ~b\r~n", [Line, Col]); + ["0", Col0, LineAndButton] -> + {Line, Button} = string:to_integer(LineAndButton), + {Col, ""} = string:to_integer(Col0), + case Button of + "M" -> + io:format("Mouse: Left button down, line ~b, column ~b\r~n", [Line, Col]); + "m" -> + io:format("Mouse: Left button up, line ~b, column ~b\r~n", [Line, Col]) + end; + ["1", Col0, LineAndButton] -> + {Line, Button} = string:to_integer(LineAndButton), + {Col, ""} = string:to_integer(Col0), + case Button of + "M" -> + io:format("Mouse: Middle button down, line ~b, column ~b\r~n", [Line, Col]); + "m" -> + io:format("Mouse: Middle button up, line ~b, column ~b\r~n", [Line, Col]) + end; + ["2", Col0, LineAndButton] -> + {Line, Button} = string:to_integer(LineAndButton), + {Col, ""} = string:to_integer(Col0), + case Button of + "M" -> + io:format("Mouse: Right button down, line ~b, column ~b\r~n", [Line, Col]); + "m" -> + io:format("Mouse: Right button up, line ~b, column ~b\r~n", [Line, Col]) + end; + ["64", Col0, Line0] -> + {Line, _} = string:to_integer(Line0), + {Col, ""} = string:to_integer(Col0), + io:format("Mouse: Wheel up, line ~b, column ~b\r~n", [Line, Col]); + ["65", Col0, Line0] -> + {Line, _} = string:to_integer(Line0), + {Col, ""} = string:to_integer(Col0), + io:format("Mouse: Wheel down, line ~b, column ~b\r~n", [Line, Col]); + _ -> + io:format("Mouse: unparsed mouse event: ~0p\r~n", [Attrs]) + end, + handle_hello_chars(Rest1, Context); +handle_hello_chars([Char | Rest], _Context) -> + io:format("Key: Other: ~p (rest: ~p)\r~n", [Char, Rest]), + stop; +handle_hello_chars("", _Context) -> + ok. + +split_escape_sequence([Char | Rest], Chars) when Char =:= $m orelse Char =:= $M -> + {lists:reverse([Char | Chars]), Rest}; +split_escape_sequence([$\e | _] = Rest, Chars) -> + {lists:reverse(Chars), Rest}; +split_escape_sequence([Char | Rest], Chars) -> + split_escape_sequence(Rest, [Char | Chars]). + +cmd_pager(Context) -> + ok = rabbit_cli_io:set_paging_mode(Context), + ok = io:format("Line 1~n"), + ok = io:format("Line 2~n"), + ok = io:format("(waiting)~n"), + timer:sleep(5000), + ok = io:format("Line 3 (last)~n"), + ok. + +cmd_signal(Context) -> + receive + {signal, sigwinch} -> + #{lines := Lines, cols := Cols} = rabbit_cli_io:get_window_size( + Context), + io:format("Window size: ~bx~b~n", [Cols, Lines]), + cmd_signal(Context); + {signal, siginfo} -> + ?LOG_DEBUG("CLI: siginfo"), + {current_stacktrace, Stacktrace} = erlang:process_info( + self(), current_stacktrace), + rabbit_cli_io:display_siginfo( + Context, + "RabbitMQ CLI in:~n ~p~n", + [Stacktrace]), + cmd_signal(Context) + end. + +-spec cmd_crash(#rabbit_cli{}) -> no_return(). + +cmd_crash(_) -> + erlang:exit(oops). + +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_datagrid.erl b/deps/rabbit/src/rabbit_cli_datagrid.erl new file mode 100644 index 000000000000..1dc326c731e7 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_datagrid.erl @@ -0,0 +1,132 @@ +-module(rabbit_cli_datagrid). + +-include_lib("kernel/include/logger.hrl"). + +-include("src/rabbit_cli_backend.hrl"). + +-export([argparse_def/0, + process/6]). + +-record(?MODULE, {fields_fun, + setup_stream_fun, + next_record_fun, + teardown_stream_fun, + + fields, + priv, + context}). + +argparse_def() -> + #{arguments => + [ + #{name => output, + long => "-output", + short => $o, + type => string, + nargs => 1, + help => "Write output to file "}, + #{name => format, + long => "-format", + short => $f, + type => {atom, [plain, legacy_plain, json]}, + default => plain, + help => "Format output acccording to "}, + #{name => fields, + type => binary, + nargs => list, + required => false, + help => "Fields to include"} + ] + }. + +process( + FieldsFun, SetupStreamFun, NextRecordFun, TeardownStreamFun, + Priv, Context) -> + State = #?MODULE{fields_fun = FieldsFun, + setup_stream_fun = SetupStreamFun, + next_record_fun = NextRecordFun, + teardown_stream_fun = TeardownStreamFun, + + priv = Priv, + context = Context}, + process_fields(State). + +process_fields(#?MODULE{fields_fun = FieldsFun, priv = Priv} = State) -> + maybe + {ok, Fields, Priv1} ?= FieldsFun(Priv), + State1 = State#?MODULE{fields = Fields, + priv = Priv1}, + + {ok, State2} ?= format_fields(State1), + start_stream(State2) + end. + +start_stream( + #?MODULE{setup_stream_fun = SetupStreamFun, + priv = Priv} = State) -> + maybe + {ok, Priv1} ?= SetupStreamFun(Priv), + State1 = State#?MODULE{priv = Priv1}, + process_records(State1) + end. + +process_records( + #?MODULE{next_record_fun = NextRecordFun, priv = Priv} = State) -> + case NextRecordFun(Priv) of + {ok, Record, Priv1} when is_list(Record) -> + maybe + State1 = State#?MODULE{priv = Priv1}, + {ok, State2} ?= format_record(Record, State1), + process_records(State2) + end; + {ok, none, Priv1} -> + State1 = State#?MODULE{priv = Priv1}, + stop_stream(State1) + end. + +stop_stream(#?MODULE{teardown_stream_fun = TeardownStreamFun, priv = Priv}) -> + TeardownStreamFun(Priv). + +format_fields(#?MODULE{fields = Fields} = State) -> + Fields1 = [rabbit_misc:format("~s", [Name]) || #{name := Name} <- Fields], + Fields2 = string:join(Fields1, "\t"), + io:format("~ts~n", [Fields2]), + {ok, State}. + +format_record(Record, #?MODULE{fields = Fields} = State) -> + Values1 = format_values(Fields, Record, State), + Values2 = string:join(Values1, "\t"), + io:format("~ts~n", [Values2]), + {ok, State}. + +format_values(Fields, Values, State) -> + format_values(Fields, Values, State, []). + +format_values([#{type := string} | Rest1], [Value | Rest2], State, Acc) -> + String = io_lib:format("~ts", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := binary} | Rest1], [Value | Rest2], State, Acc) -> + String = io_lib:format("~-20.. ts", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := integer} | Rest1], [Value | Rest2], State, Acc) -> + String = io_lib:format("~b", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := boolean} | Rest1], [Value | Rest2], State, Acc) -> + #?MODULE{context = #rabbit_cli{legacy = Legacy}} = State, + String = case Legacy of + false -> + io_lib:format("~ts", [if Value -> "☑"; true -> "☐" end]); + true -> + io_lib:format("~ts", [Value]) + end, + Acc1 = [String | Acc], + format_values(Rest1, Rest2, State, Acc1); +format_values([#{type := term} | Rest1], [Value | Rest2], State, Acc) -> + String = io_lib:format("~0p", [Value]), + Acc1 = [String | Acc], + format_values(Rest1, Rest2, State, Acc1); +format_values([], [], _State, Acc) -> + lists:reverse(Acc). diff --git a/deps/rabbit/src/rabbit_cli_frontend.erl b/deps/rabbit/src/rabbit_cli_frontend.erl new file mode 100644 index 000000000000..e57e861b1c65 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_frontend.erl @@ -0,0 +1,472 @@ +-module(rabbit_cli_frontend). + +-behaviour(gen_event). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-include("src/rabbit_cli_backend.hrl"). + +-export([main/1, + noop/1]). +-export([init/1, + handle_call/2, + handle_event/2, + terminate/2, + code_change/3]). + +-record(?MODULE, {scriptname, + connection, + backend, + pager}). + +-spec main(Args) -> no_return() when + Args :: argparse:args(). + +main(Args) -> + ScriptName = escript:script_name(), + add_rabbitmq_code_path(ScriptName), + configure_logging(), + + Ret = run_cli(ScriptName, Args), + ?LOG_DEBUG("CLI: run_cli() return value: ~p", [Ret]), + + flush_log_messages(), + 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). + +-define(LOG_HANDLER_NAME, rmq_cli). + +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(?LOG_HANDLER_NAME, rabbit_logger_std_h, Config), + ok = logger:remove_handler(default), + ok. + +flush_log_messages() -> + _ = rabbit_logger_std_h:filesync(?LOG_HANDLER_NAME), + ok. + +%% ------------------------------------------------------------------- +%% Preparation for remote command execution. +%% ------------------------------------------------------------------- + +run_cli(ScriptName, Args) -> + ProgName0 = filename:basename(ScriptName, ".bat"), + ProgName1 = filename:basename(ProgName0, ".escript"), + case is_legacy_progname(ProgName1) of + false -> + run_cli(ScriptName, ProgName1, Args); + true -> + run_legacy_cli(Args) + end. + +is_legacy_progname("rabbitmqctl") -> + true; +is_legacy_progname("rabbitmq-diagnostics") -> + true; +is_legacy_progname("rabbitmq-plugins") -> + true; +is_legacy_progname("rabbitmq-queues") -> + true; +is_legacy_progname("rabbitmq-streams") -> + true; +is_legacy_progname("rabbitmq-upgrade") -> + true; +is_legacy_progname(_Progname) -> + false. + +run_cli(ScriptName, ProgName, Args) -> + Terminal = collect_terminal_info(), + configure_signal_handler(), + Priv = #?MODULE{scriptname = ScriptName}, + Context = #rabbit_cli{progname = ProgName, + args = Args, + os = os:type(), + env = os:env(), + terminal = Terminal, + priv = Priv}, + init_local_args(Context). + +run_legacy_cli(Args) -> + 'Elixir.RabbitMQCtl':main(Args). + +collect_terminal_info() -> + IoOpts = io:getopts(), + Term = eterminfo:get_term_type_or_default(), + TermInfo = case eterminfo:read_by_infocmp(Term) of + {ok, TI} -> + TI; + _ -> + case eterminfo:read_by_file(Term) of + {ok, TI} -> + TI; + _ -> + undefined + end + end, + #{stdout => proplists:get_value(stdout, IoOpts), + stderr => proplists:get_value(stderr, IoOpts), + stdin => proplists:get_value(stdin, IoOpts), + + name => Term, + info => TermInfo}. + +configure_signal_handler() -> + gen_event:add_handler(erl_signal_server, ?MODULE, self()), + Signals = [sigwinch, siginfo], + lists:foreach( + fun(Signal) -> + try + os:set_signal(Signal, handle) + catch + _:badarg -> + ?LOG_DEBUG("Signal ~s not supported", [Signal]) + end + end, Signals), + ok. + +init_local_args(Context) -> + maybe + LocalArgparseDef = initial_argparse_def(), + 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, priv = Priv} = Context) -> + Ret = case ArgMap of + #{node := NodenameOrUri} -> + rabbit_cli_transport:connect(NodenameOrUri); + _ -> + rabbit_cli_transport:connect() + end, + Priv1 = case Ret of + {ok, Connection} -> + Priv#?MODULE{connection = Connection}; + {error, Reason} -> + ?LOG_DEBUG( + "CLI: failed to establish a connection to a RabbitMQ " + "node: ~0p", + [Reason]), + Priv#?MODULE{connection = none} + end, + ClientInfo = rabbit_cli_transport:get_client_info( + Priv1#?MODULE.connection), + Context1 = Context#rabbit_cli{client = ClientInfo, + priv = Priv1}, + run_command(Context1). + +%% ------------------------------------------------------------------- +%% 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{priv = #?MODULE{connection = Connection} = Priv} = Context) + when Connection =/= none -> + maybe + process_flag(trap_exit, true), + ContextMap = context_to_map(Context), + {ok, Backend} ?= rabbit_cli_transport:run_command( + Connection, ContextMap), + Priv1 = Priv#?MODULE{backend = Backend}, + Context1 = Context#rabbit_cli{priv = Priv1}, + main_loop(Context1) + end; +run_command(#rabbit_cli{} = Context) -> + %% FIXME: Load applications first, otherwise module attributes are + %% unavailable. + maybe + process_flag(trap_exit, true), + prepare_offline_exec(Context), + ContextMap = context_to_map(Context), + {ok, _Backend} ?= rabbit_cli_backend:run_command(ContextMap, self()), + main_loop(Context) + end. + +prepare_offline_exec(_Context) -> + ?LOG_DEBUG("CLI: prepare for offline execution"), + Env = rabbit_env:get_context(), + rabbit_env:context_to_code_path(Env), + rabbit_env:context_to_app_env_vars(Env), + PluginsDir = rabbit_plugins:plugins_dir(), + Plugins = rabbit_plugins:plugin_names( + rabbit_plugins:list(PluginsDir, true)), + Apps = [rabbit_common, rabbit | Plugins], + lists:foreach( + fun(App) -> _ = application:load(App) end, + Apps), + ?LOG_DEBUG("CLI: ready for offline execution: ~p", [application:loaded_applications()]), + ok. + +context_to_map(Context) -> + Fields = [Field || Field <- record_info(fields, rabbit_cli), + %% We don't need or want to communicate anything that + %% is private to the backend. + Field =/= 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{priv = #?MODULE{connection = Connection, + backend = Backend, + pager = Pager} = Priv} = Context) -> + ?LOG_DEBUG("CLI: frontend main loop (pager: ~0p)...", [Pager]), + Timeout = case is_port(Pager) of + false -> + infinity; + true -> + 100 + end, + receive + {'EXIT', Pager, Reason} -> + ?LOG_DEBUG("CLI: EXIT signal from pager: ~p", [Reason]), + Priv1 = Priv#?MODULE{pager = undefined}, + Context1 = Context#rabbit_cli{priv = Priv1}, + terminate_cli(Reason, Context1); + {'EXIT', LinkedPid, Reason} -> + ?LOG_DEBUG( + "CLI: EXIT signal from linked process ~0p: ~p", + [LinkedPid, Reason]), + case Pager of + undefined -> + terminate_cli(Reason, Context); + _ -> + ?LOG_DEBUG("CLI: waiting for pager to exit"), + main_loop(Context) + end; + {frontend_request, From, Request} -> + {reply, Reply, Context1} = handle_request(Request, Context), + _ = rabbit_cli_transport:gen_reply(Connection, From, Reply), + main_loop(Context1); + {io_request, From, ReplyAs, Request} + when element(1, Request) =:= put_chars andalso is_port(Pager) -> + Chars0 = case Request of + {put_chars, unicode, M, F, A} -> + erlang:apply(M, F, A); + {put_chars, unicode, C} -> + C + end, + Chars1 = re:replace(Chars0, "\n", "\r\n"), + Bin = unicode:characters_to_binary(Chars1), + erlang:port_command(Pager, Bin), + IoReply = {io_reply, ReplyAs, ok}, + From ! IoReply, + main_loop(Context); + {io_request, _From, _ReplyAs, _Request} = IoRequest -> + GroupLeader = erlang:group_leader(), + GroupLeader ! IoRequest, + main_loop(Context); + {signal, Signal} = Event -> + ?LOG_DEBUG("CLI: got Unix signal: ~ts", [Signal]), + _ = rabbit_cli_transport:send(Connection, Backend, Event), + main_loop(Context); + Info -> + ?LOG_ALERT("CLI: unknown info: ~0p", [Info]), + main_loop(Context) + after Timeout -> + erlang:port_command(Pager, <<>>), + main_loop(Context) + end. + +terminate_cli(Reason, _Context) -> + ?LOG_DEBUG("CLI: frontend terminating: ~0p", [Reason]), + ok. + +handle_request({read_file, Filename}, Context) -> + {reply, file:read_file(Filename), Context}; +handle_request({write_file, Filename, Bytes}, Context) -> + {reply, file:write_file(Filename, Bytes), Context}; +handle_request(set_interactive_mode, Context) -> + Ret = shell:start_interactive({noshell, raw}), + ?LOG_DEBUG("CLI: interactive mode: ~p", [Ret]), + {reply, Ret, Context}; +handle_request( + set_paging_mode, #rabbit_cli{env = Env, priv = Priv} = Context) -> + Cmd = case proplists:get_value("PAGER", Env) of + Value when is_list(Value) -> + Value; + undefined -> + "less" + end, + ?LOG_DEBUG("CLI: start pager \"~ts\"", [Cmd]), + Pager = erlang:open_port( + {spawn, Cmd}, + [stream, exit_status, binary, use_stdio, out, hide, {env, Env}]), + Priv1 = Priv#?MODULE{pager = Pager}, + Context1 = Context#rabbit_cli{priv = Priv1}, + {reply, ok, Context1}; +handle_request({display_siginfo, {Format, Args}}, Context) -> + io:format(standard_error, Format, Args), + {reply, ok, Context}; +handle_request({display_siginfo, String}, Context) -> + io:format(standard_error, "~ts", [String]), + {reply, ok, Context}; +handle_request(get_window_size, Context) -> + Size = get_window_size(Context), + {reply, Size, Context}. + +get_window_size(#rabbit_cli{env = Env}) -> + DefaultSize = #{lines => 25, cols => 80}, + Cmd = "stty size", + Port = erlang:open_port( + {spawn, Cmd}, + [stream, use_stdio, in, hide, {env, Env}]), + get_window_size_loop(Port, DefaultSize). + +get_window_size_loop(Port, Size) -> + receive + {Port, {data, Output}} -> + [LinesStr, ColsStr] = string:lexemes(string:trim(Output), " "), + Lines = list_to_integer(LinesStr), + Cols = list_to_integer(ColsStr), + Size1 = Size#{lines => Lines, + cols => Cols}, + get_window_size_loop(Port, Size1); + {'EXIT', Port, _Reason} -> + Size + end. + +%% ------------------------------------------------------------------- +%% gen_event callbacks (signal handler). +%% ------------------------------------------------------------------- + +init(Parent) -> + {ok, Parent}. + +handle_call(Request, Parent) -> + ?LOG_DEBUG("CLI: (signal) unknown request: ~0p", [Request]), + {ok, ok, Parent}. + +handle_event(Signal, Parent) + when Signal =:= sigwinch orelse Signal =:= siginfo -> + ?LOG_DEBUG("CLI: (signal) signal = ~0p", [Signal]), + Parent ! {signal, Signal}, + {ok, Parent}; +handle_event(Event, Parent) -> + ?LOG_DEBUG("CLI: (signal) unknown event: ~0p", [Event]), + {ok, Parent}. + +terminate(Reason, _Parent) -> + ?LOG_DEBUG("CLI: (signal) terminate: ~0p", [Reason]), + ok. + +code_change(_OldVsn, Parent, _Extra) -> + {ok, Parent}. diff --git a/deps/rabbit/src/rabbit_cli_http_client.erl b/deps/rabbit/src/rabbit_cli_http_client.erl new file mode 100644 index 000000000000..e24337d64c22 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_http_client.erl @@ -0,0 +1,183 @@ +-module(rabbit_cli_http_client). + +-behaviour(gen_statem). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/1, + run_command/2, + gen_reply/3, + 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() | undefined, + delayed_requests = [] :: list(), + io_requests = #{} :: map(), + caller :: pid()}). + +start_link(Uri) -> + Caller = self(), + gen_statem:start_link(?MODULE, #{uri => Uri, caller => Caller}, []). + +run_command(Client, ContextMap) -> + gen_statem:call(Client, {?FUNCTION_NAME, ContextMap}). + +gen_reply(Client, From, Reply) -> + gen_statem:cast(Client, {?FUNCTION_NAME, From, Reply}). + +send(Client, Dest, Msg) -> + gen_statem:cast(Client, {?FUNCTION_NAME, Dest, Msg}). + +%% ------------------------------------------------------------------- +%% gen_statem callbacks. +%% ------------------------------------------------------------------- + +init(#{uri := Uri, caller := Caller}) -> + maybe + #{host := Host, port := Port} = UriMap = uri_string:parse(Uri), + + {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, + caller = Caller, + 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}; +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}; +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}. + +%% ------------------------------------------------------------------- +%% Internal functions. +%% ------------------------------------------------------------------- + +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( + {frontend_request, _From, _Request} = FrontendRequest, + #?MODULE{caller = Caller} = Data) -> + Caller ! FrontendRequest, + {noreply, Data}; +handle_request( + {io_request, From, ReplyAs, Request}, + #?MODULE{caller = Caller, + io_requests = IoRequests} = Data) -> + ProxyRef = erlang:make_ref(), + ProxyIoRequest = {io_request, self(), ProxyRef, Request}, + Caller ! 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_server.erl b/deps/rabbit/src/rabbit_cli_http_server.erl new file mode 100644 index 000000000000..bee9f26a1d6b --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_http_server.erl @@ -0,0 +1,219 @@ +-module(rabbit_cli_http_server). + +-behaviour(gen_server). +-behaviour(cowboy_websocket). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/0]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + config_change/3]). +-export([init/2, + websocket_init/1, + websocket_handle/2, + websocket_info/2, + terminate/3]). + +-record(?MODULE, {listeners = [] :: [{proto(), inet:port_number(), pid()}]}). + +-type proto() :: erldist | http. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, #{}, []). + +%% ------------------------------------------------------------------- +%% Top-level gen_server. +%% ------------------------------------------------------------------- + +init(_) -> + process_flag(trap_exit, true), + case start_listeners() of + {ok, []} -> + ignore; + {ok, Listeners} -> + State = #?MODULE{listeners = Listeners}, + {ok, State, hibernate} + end. + +handle_call(Request, From, State) -> + ?LOG_DEBUG("CLI: unhandled call from ~0p: ~p", [From, Request]), + {reply, ok, State}. + +handle_cast(Request, State) -> + ?LOG_DEBUG("CLI: unhandled cast: ~p", [Request]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG_DEBUG("CLI: unhandled info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #?MODULE{listeners = Listeners}) -> + stop_listeners(Listeners). + +%% ------------------------------------------------------------------- +%% HTTP listeners management. +%% ------------------------------------------------------------------- + +start_listeners() -> + case application:get_env(rabbit, cli_listeners) of + undefined -> + ?LOG_INFO("CLI: no HTTP(S) listeners started"), + {ok, []}; + {ok, Listeners} when is_list(Listeners) -> + start_listeners(Listeners, []) + end. + +start_listeners( + [{[_, _, "http" = Proto], Port} | Rest], Result) when is_integer(Port) -> + ?LOG_INFO("CLI: starting \"~s\" listener on TCP port ~b", [Proto, Port]), + Name = list_to_binary(io_lib:format("cli_listener_~s_~b", [Proto, Port])), + case start_listener(Name, Port) of + {ok, Pid} -> + Result1 = [{Proto, Port, Pid} | Result], + start_listeners(Rest, Result1); + {error, Reason} -> + ?LOG_ERROR( + "CLI: failed to start \"~s\" listener on TCP port ~b: ~0p", + [Proto, Port, Reason]), + start_listeners(Rest, Result) + end; +start_listeners([], Result) -> + Result1 = lists:reverse(Result), + {ok, Result1}. + +start_listener(Name, Port) -> + Dispatch = cowboy_router:compile([{'_', [{'_', ?MODULE, #{}}]}]), + cowboy:start_clear(Name, + [{port, Port}], + #{env => #{dispatch => Dispatch}} + ). + +stop_listeners([{Proto, Port, Pid} | Rest]) -> + ?LOG_INFO("CLI: stopping \"~s\" listener on TCP port ~b", [Proto, Port]), + _ = cowboy:stop_listener(Pid), + stop_listeners(Rest); +stop_listeners([]) -> + ok. + +config_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% ------------------------------------------------------------------- +%% Cowboy handler. +%% ------------------------------------------------------------------- + +init(#{method := <<"GET">>} = Req, State) -> + UpgradeHeader = cowboy_req:header(<<"upgrade">>, Req), + case UpgradeHeader of + <<"websocket">> -> + {cowboy_websocket, Req, State, #{idle_timeout => 30000}}; + _ -> + case Req of + #{path := Path} + when Path =:= <<"">> orelse Path =:= <<"/index.html">> -> + Req1 = reply_with_help(Req, 200), + {ok, Req1, State}; + _ -> + Req1 = reply_with_help(Req, 404), + {ok, Req1, State} + end + end; +init(Req, State) -> + Req1 = reply_with_help(Req, 405), + {ok, Req1, State}. + +websocket_init(State) -> + process_flag(trap_exit, true), + erlang:group_leader(self(), self()), + {ok, State}. + +websocket_handle({binary, RequestBin}, State) -> + Request = binary_to_term(RequestBin), + ?LOG_DEBUG("CLI: received HTTP message from client: ~p", [Request]), + try + case handle_request(Request, State) of + {reply, Reply, State1} -> + ReplyBin = term_to_binary(Reply), + Frame1 = {binary, ReplyBin}, + {[Frame1], State1}; + {noreply, State1} -> + {ok, State1} + end + catch + Class:Reason:Stacktrace -> + Exception = {call_exception, Class, Reason, Stacktrace}, + ExceptionBin = term_to_binary(Exception), + Frame2 = {binary, ExceptionBin}, + {[Frame2], State} + end; +websocket_handle(Frame, State) -> + ?LOG_DEBUG("CLI: unhandled Websocket frame: ~p", [Frame]), + {ok, State}. + +websocket_info({frontend_request, _From, _Request} = FrontendRequest, State) -> + FrontendRequestBin = term_to_binary(FrontendRequest), + Frame = {binary, FrontendRequestBin}, + {[Frame], State}; +websocket_info({io_request, _From, _ReplyAs, _Request} = IoRequest, State) -> + IoRequestBin = term_to_binary(IoRequest), + Frame = {binary, IoRequestBin}, + {[Frame], State}; +websocket_info({'EXIT', _Pid, _Reason} = Exit, State) -> + ExitBin = term_to_binary(Exit), + Frame = {binary, ExitBin}, + {[Frame, close], State}. + +terminate(Reason, _Req, State) -> + ?LOG_DEBUG("CLI: HTTP server terminating: ~0p", [Reason]), + case State of + #{backend := Backend} -> + _ = catch erlang:exit(Backend, Reason); + _ -> + ok + end, + ok. + +%% ------------------------------------------------------------------- +%% Internal functions. +%% ------------------------------------------------------------------- + +reply_with_help(Req, Code) -> + PrivDir = code:priv_dir(rabbit), + HelpFilename = filename:join(PrivDir, "cli_http_help.html"), + Body = case file:read_file(HelpFilename) of + {ok, Content} -> + Content; + {error, _} -> + <<>> + end, + cowboy_req:reply( + Code, #{<<"content-type">> => <<"text/html; charset=utf-8">>}, Body, + Req). + +handle_request({call, From, Command}, State) -> + {Ret, State1} = handle_command(Command, State), + Reply = {call_ret, From, Ret}, + {reply, Reply, State1}; +handle_request({cast, Command}, State) -> + {_, State1} = handle_command(Command, State), + {noreply, State1}. + +handle_command({run_command, ContextMap}, State) -> + Caller = self(), + case rabbit_cli_backend:run_command(ContextMap, Caller) of + {ok, Backend} = Ret -> + State1 = State#{backend => Backend}, + {Ret, State1}; + {error, _} = Error -> + {Error, State} + end; +handle_command({gen_reply, From, Reply}, State) -> + Ret = gen:reply(From, Reply), + {Ret, State}; +handle_command({send, Dest, Msg}, State) -> + Ret = erlang:send(Dest, Msg), + {Ret, State}. diff --git a/deps/rabbit/src/rabbit_cli_io.erl b/deps/rabbit/src/rabbit_cli_io.erl new file mode 100644 index 000000000000..4de1906789bb --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_io.erl @@ -0,0 +1,428 @@ +-module(rabbit_cli_io). + +-include_lib("kernel/include/logger.hrl"). + +-include_lib("rabbit_common/include/resource.hrl"). + +-include("src/rabbit_cli_backend.hrl"). + +-export([argparse_def/1, + read_stdin/1, + read_file/2, + write_file/3, + set_interactive_mode/1, + set_paging_mode/1, + display_siginfo/2, display_siginfo/3, + get_window_size/1, + supports_colors/1]). +-export([start_link/1, + stop/1, + display_help/1, + format/3, + start_record_stream/4, + push_new_record/3, + end_record_stream/2, + send_keyboard_input/3]). +-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 = []}). + +% 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 "} + ] + }; +argparse_def(file_output) -> + #{arguments => + [ + #{name => output, + long => "-output", + short => $o, + type => string, + nargs => 1, + help => "Write output to file "} + ] + }. + +read_stdin(Context) -> + read_stdin(Context, []). + +read_stdin(Context, Data) -> + case io:get_chars("", 4096) of + Chunk when is_list(Chunk) -> + Data1 = [Data, Chunk], + read_stdin(Context, Data1); + eof -> + {ok, list_to_binary(Data)}; + {error, _} = Error -> + Error + end. + +read_file(Context, Filename) -> + Request = {?FUNCTION_NAME, Filename}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +write_file(Context, Filename, Bytes) -> + Request = {?FUNCTION_NAME, Filename, Bytes}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +set_interactive_mode(Context) -> + ?LOG_DEBUG("CLI: request interactive mode"), + Request = ?FUNCTION_NAME, + rabbit_cli_backend:send_frontend_request(Context, Request). + +set_paging_mode(Context) -> + ?LOG_DEBUG("CLI: request paging mode"), + Request = ?FUNCTION_NAME, + rabbit_cli_backend:send_frontend_request(Context, Request). + +display_siginfo(Context, String) -> + Request = {?FUNCTION_NAME, String}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +display_siginfo(Context, Format, Args) -> + Request = {?FUNCTION_NAME, {Format, Args}}, + rabbit_cli_backend:send_frontend_request(Context, Request). + +get_window_size(Context) -> + Request = ?FUNCTION_NAME, + rabbit_cli_backend:send_frontend_request(Context, Request). + +supports_colors(#rabbit_cli{env = Env, terminal = #{info := TermInfo}}) -> + ?LOG_DEBUG("Env: ~p", [Env]), + case proplists:get_value("COLORTERM", Env) of + "truecolor" -> + {true, truecolor}; + "24bit" -> + {true, truecolor}; + _ -> + case eterminfo:tigetnum_m(TermInfo, colors) of + Colors when is_integer(Colors) andalso Colors >= 0 -> + {true, Colors}; + _ -> + false + end + end. + +%% ------------------------------------------------------------------- +%% OLD CODE (to remove). +%% ------------------------------------------------------------------- + +start_link(Progname) -> + gen_server:start_link(rabbit_cli_io, #{progname => Progname}, []). + +stop(IO) -> + MRef = erlang:monitor(process, IO), + _ = gen_server:call(IO, stop), + receive + {'DOWN', MRef, _, _, _Reason} -> + ok + end. + +display_help(#{io := {transport, Transport}} = Context) -> + Transport ! {io_cast, {?FUNCTION_NAME, Context}}; +display_help(#{io := IO} = Context) -> + 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..7193dacb6501 --- /dev/null +++ b/deps/rabbit/src/rabbit_cli_transport.erl @@ -0,0 +1,117 @@ +-module(rabbit_cli_transport). + +-include_lib("kernel/include/logger.hrl"). + +-export([connect/0, connect/1, + get_client_info/1, + run_command/2, + gen_reply/3, + send/3]). + +-record(?MODULE, {type :: erldist | http, + peer :: atom() | pid()}). + +connect() -> + Nodename = guess_rabbitmq_nodename(), + connect(erldist, Nodename). + +connect(NodenameOrUri) -> + Proto = determine_proto(NodenameOrUri), + connect(Proto, NodenameOrUri). + +connect(erldist = Proto, Nodename) -> + maybe + Nodename1 = complete_nodename(Nodename), + ?LOG_DEBUG( + "CLI: connect to node ~s using Erlang distribution", + [Nodename1]), + + %% FIXME: Handle short vs. long names. + {ok, _} ?= net_kernel:start(undefined, #{name_domain => shortnames}), + + %% Can we reach the remote node? + case net_kernel:connect_node(Nodename1) of + true -> + Connection = #?MODULE{type = Proto, + peer = Nodename1}, + {ok, Connection}; + false -> + {error, noconnection} + end + end; +connect(http = Proto, Uri) -> + maybe + ?LOG_DEBUG( + "CLI: connect to URI ~s using HTTP", + [Uri]), + {ok, Client} ?= rabbit_cli_http_client:start_link(Uri), + Connection = #?MODULE{type = Proto, + peer = Client}, + {ok, Connection} + end. + +guess_rabbitmq_nodename() -> + case net_adm:names() of + {ok, NamesAndPorts} -> + Names0 = [Name || {Name, _Port} <- NamesAndPorts], + Names1 = lists:sort(Names0), + Names2 = lists:filter( + fun + ("rabbit" ++ _) -> true; + (_) -> false + end, Names1), + case Names2 of + [First | _] -> + First; + [] -> + "rabbit" + end; + {error, address} -> + "rabbit" + end. + +determine_proto(NodenameOrUri) -> + case re:run(NodenameOrUri, "://", [{capture, none}]) of + nomatch -> + erldist; + match -> + http + end. + +complete_nodename(Nodename) -> + case re:run(Nodename, "@", [{capture, none}]) of + nomatch -> + {ok, ThisHost} = inet:gethostname(), + list_to_atom(Nodename ++ "@" ++ ThisHost); + match -> + list_to_atom(Nodename) + end. + +get_client_info(#?MODULE{type = Proto}) -> + {ok, Hostname} = inet:gethostname(), + #{hostname => Hostname, + proto => Proto}; +get_client_info(none) -> + {ok, Hostname} = inet:gethostname(), + #{hostname => Hostname, + proto => none}. + +run_command(#?MODULE{type = erldist, peer = Node}, ContextMap) -> + Caller = self(), + erpc:call(Node, rabbit_cli_backend, run_command, [ContextMap, Caller]); +run_command(#?MODULE{type = http, peer = Client}, ContextMap) -> + rabbit_cli_http_client:run_command(Client, ContextMap). + +gen_reply(#?MODULE{type = erldist}, From, Reply) -> + gen:reply(From, Reply); +gen_reply(#?MODULE{type = http, peer = Client}, From, Reply) -> + rabbit_cli_http_client:gen_reply(Client, From, Reply); +gen_reply(none, From, Reply) -> + gen:reply(From, Reply). + +send(#?MODULE{type = erldist}, Dest, Msg) -> + erlang:send(Dest, Msg); +send(#?MODULE{type = http, peer = Client}, Dest, Msg) -> + rabbit_cli_http_client:send(Client, Dest, Msg); +send(none, Dest, Msg) -> + erlang:send(Dest, Msg). diff --git a/deps/rabbit/src/rabbit_definitions.erl b/deps/rabbit/src/rabbit_definitions.erl index 884466a81787..7115d8ba61fc 100644 --- a/deps/rabbit/src/rabbit_definitions.erl +++ b/deps/rabbit/src/rabbit_definitions.erl @@ -29,8 +29,12 @@ %% * rabbit_definitions_import_http %% * rabbit_definitions_hashing -module(rabbit_definitions). + +-include_lib("kernel/include/logger.hrl"). -include_lib("rabbit_common/include/rabbit.hrl"). +-include("src/rabbit_cli_backend.hrl"). + -export([boot/0]). %% automatic import on boot -export([ @@ -56,6 +60,10 @@ ]). -export([decode/1, decode/2, args/1, validate_definitions/1]). +%% CLI commands. +-export([cmd_import_definitions/1, + cmd_export_definitions/1]). + %% for tests -export([ maybe_load_definitions_from_local_filesystem_if_unchanged/3, @@ -90,6 +98,18 @@ -export_type([definition_object/0, definition_list/0, definition_category/0, definitions/0]). +-rabbitmq_command( + {#{cli => ["import", "definitions"]}, + [{rabbit_cli_io, argparse_def, [file_input]}, + #{help => "Import definitions", + handler => {?MODULE, cmd_import_definitions}}]}). + +-rabbitmq_command( + {#{cli => ["export", "definitions"]}, + [{rabbit_cli_io, argparse_def, [file_output]}, + #{help => "Export definitions", + handler => {?MODULE, cmd_export_definitions}}]}). + -define(IMPORT_WORK_POOL, definition_import_pool). boot() -> @@ -1159,3 +1179,70 @@ topic_permission_definition(P0) -> tags_as_binaries(Tags) -> [to_binary(T) || T <- Tags]. + +%% ------------------------------------------------------------------- +%% CLI commands. +%% ------------------------------------------------------------------- + +cmd_import_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> + case ArgMap of + #{input := "-"} -> + import_from_stdin(Context); + #{input := Filename} -> + import_from_file(Context, Filename); + _ -> + import_from_stdin(Context) + end. + +import_from_file(Context, Filename) -> + case rabbit_cli_io:read_file(Context, Filename) of + {ok, Data} -> + do_import(Context, Data); + {error, _} = Error -> + Error + end. + +import_from_stdin(Context) -> + case rabbit_cli_io:read_stdin(Context) of + {ok, Data} -> + do_import(Context, Data); + {error, _} = Error -> + Error + end. + +do_import(_Context, Data) -> + try + Json = json:decode(Data), + import_parsed(Json) + catch + error:unexpected_end = Reason -> + {error, Reason}; + error:{invalid_byte, _Byte} = Reason -> + {error, Reason}; + error:{unexpected_sequence, _Bytes} = Reason -> + {error, Reason} + end. + +cmd_export_definitions(#rabbit_cli{arg_map = ArgMap} = Context) -> + Defs = all_definitions(), + case ArgMap of + #{output := "-"} -> + export_to_stdin(Context, Defs); + #{output := Filename} -> + export_to_file(Context, Defs, Filename); + _ -> + export_to_stdin(Context, Defs) + end. + +export_to_file(Context, Defs, Filename) -> + Json = json:encode(Defs), + rabbit_cli_io:write_file(Context, Filename, Json). + +export_to_stdin(#rabbit_cli{terminal = Terminal}, Defs) -> + Json = case Terminal of + #{stdout := true} -> + json:format(Defs); + _ -> + json:encode(Defs) + end, + io:format("~ts~n", [Json]). diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index 8a57123ad67f..a109cd0ff352 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -8,6 +8,8 @@ -module(rabbit_exchange). -include_lib("rabbit_common/include/rabbit.hrl"). +-include("src/rabbit_cli_backend.hrl"). + -export([recover/1, policy_changed/2, callback/4, declare/7, assert_equivalence/6, assert_args_equivalence/2, check_type/1, exists/1, lookup/1, lookup_many/1, lookup_or_die/1, list/0, list/1, lookup_scratch/2, @@ -19,6 +21,9 @@ -export([serialise_events/1]). -export([serial/1, peek_serial/1]). +%% CLI commands. +-export([cmd_list_exchanges/1]). + %%---------------------------------------------------------------------------- -deprecated([{route, 2, "Use route/3 instead"}]). @@ -38,6 +43,36 @@ policy, user_who_performed_action]). -define(DEFAULT_EXCHANGE_NAME, <<>>). +-rabbitmq_command( + {#{cli => ["list", "exchanges"]}, + [rabbit_cli_datagrid, + #{help => "List exchanges", + arguments => + [ + #{name => fields, + %% FIXME: Exclude user_who_performed_action + type => {atom, ?INFO_KEYS}, + nargs => list, + required => false, + help => "Fields to include"} + ], + handler => {?MODULE, cmd_list_exchanges}}]}). +-rabbitmq_command( + {#{cli => ["list_exchanges"]}, + [rabbit_cli_datagrid, + #{help => "List exchanges", + arguments => + [ + #{name => fields, + %% FIXME: Exclude user_who_performed_action + type => {atom, ?INFO_KEYS}, + nargs => list, + required => false, + help => "Fields to include"} + ], + handler => {?MODULE, cmd_list_exchanges}, + legacy => true}]}). + -spec recover(rabbit_types:vhost()) -> [name()]. recover(VHost) -> @@ -561,3 +596,126 @@ type_to_route_fun(T) -> FunArity -> FunArity end. + +%% ------------------------------------------------------------------- +%% CLI commands. +%% ------------------------------------------------------------------- + +cmd_list_exchanges( + #rabbit_cli{arg_map = ArgMap, + terminal = Terminal, + legacy = Legacy} = Context) -> + VHost = <<"/">>, %% TODO: Take from args and use it. + InfoKeys = rabbit_exchange:info_keys(), + Fields0 = case ArgMap of + #{fields := Arg} -> + Arg; + _ -> + [name, type] + end, + Fields1 = lists:filter( + fun(Field) -> + lists:member(Field, InfoKeys) + end, Fields0), + Priv = #{fields => Fields1}, + + case Legacy of + false -> + case Terminal of + #{stdout := true} -> + ok = rabbit_cli_io:set_paging_mode(Context); + _ -> + ok + end, + + case rabbit_cli_io:supports_colors(Context) of + {true, truecolor} -> + io:format( + "Listing \033[;2;193;18;31mexchanges\033[0m " + "for vhost \033[;2;102;155;188m~ts\033[0m:~n", + [VHost]); + {true, _} -> + io:format( + "Listing \033[1mexchanges\033[0m " + "for vhost \033[1m~ts\033[0m:~n", + [VHost]); + false -> + io:format( + "Listing exchanges for vhost ~ts:~n", + [VHost]) + end; + true -> + io:format("Listing exchanges for vhost ~ts ...~n", [VHost]) + end, + + %% Start datagrid with callbacks. + rabbit_cli_datagrid:process( + fun exchanges_fields/1, + fun exchanges_setup_stream/1, + fun exchanges_next_record/1, + fun exchanges_teardown_stream/1, + Priv, Context). + +exchanges_fields( + #{fields := Fields} = Priv) -> + Fields1 = lists:map( + fun + (name = Key) -> + #{name => Key, type => string}; + (type = Key) -> + #{name => Key, type => string}; + (durable = Key) -> + #{name => Key, type => boolean}; + (auto_delete = Key) -> + #{name => Key, type => boolean}; + (internal = Key) -> + #{name => Key, type => boolean}; + (arguments = Key) -> + #{name => Key, type => term}; + (policy = Key) -> + #{name => Key, type => string}; + (Key) -> + #{name => Key, type => term} + end, Fields), + {ok, Fields1, Priv}. + +exchanges_setup_stream(Priv) -> + Exchanges = rabbit_exchange:list(), + case Exchanges of + [First | _] -> + Next = First#exchange.name, + {ok, Priv#{exchanges => Exchanges, next => Next}}; + [] -> + {ok, Priv#{exchanges => Exchanges}} + end. + +exchanges_teardown_stream(_Priv) -> + ok. + +exchanges_next_record(#{exchanges := Exchanges, next := Name} = Priv) -> + exchanges_next_record1(Exchanges, Name, Priv); +exchanges_next_record(Priv) -> + {ok, none, Priv}. + +exchanges_next_record1( + [#exchange{name = Name1} = Exchange | Rest], Name2, + #{fields := Fields} = Priv) + when Name1 =:= Name2 -> + Record0 = info(Exchange, Fields), + Record1 = lists:sublist(Record0, length(Fields)), + Record2 = [case Value of + #resource{name = N} -> + N; + _ -> + Value + end || {_Key, Value} <- Record1], + + Priv1 = case Rest of + [#exchange{name = Next} | _] -> + Priv#{next => Next}; + [] -> + maps:remove(next, Priv) + end, + {ok, Record2, Priv1}; +exchanges_next_record1([_ | Rest], Name, Priv) -> + exchanges_next_record1(Rest, Name, Priv). 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)). diff --git a/deps/rabbit_common/mk/rabbitmq-dist.mk b/deps/rabbit_common/mk/rabbitmq-dist.mk index b38ab383ba18..5f83b5a92bc4 100644 --- a/deps/rabbit_common/mk/rabbitmq-dist.mk +++ b/deps/rabbit_common/mk/rabbitmq-dist.mk @@ -200,10 +200,23 @@ do-dist:: $(DIST_EZS) $(verbose) unwanted='$(filter-out $(DIST_EZS) $(EXTRA_DIST_EZS), \ $(wildcard $(DIST_DIR)/*))'; \ test -z "$$unwanted" || (echo " RM $$unwanted" && rm -rf $$unwanted) + $(verbose) unzip -d "$(DIST_DIR)" \ + -x 'csv*' \ + -x 'json*' \ + -x 'stdout_formatter*' \ + -o \ + $(CLI_ESCRIPTS_DIR)/rabbitmqctl CLI_SCRIPTS_LOCK = $(CLI_SCRIPTS_DIR).lock CLI_ESCRIPTS_LOCK = $(CLI_ESCRIPTS_DIR).lock +LEGACY_CLI_COMMANDS := rabbitmqctl \ + rabbitmq-diagnostics \ + rabbitmq-plugins \ + rabbitmq-queues \ + rabbitmq-streams \ + rabbitmq-upgrade + ifeq ($(MAKELEVEL),0) ifneq ($(filter-out rabbit_common amqp10_common rabbitmq_stream_common,$(PROJECT)),) # These do not depend on 'rabbit' as DEPS but may as TEST_DEPS. @@ -223,6 +236,12 @@ install-cli-scripts: | $(CLI_SCRIPTS_DIR) test -d "$(DEPS_DIR)/rabbit/scripts"; \ $(call maybe_flock,$(CLI_SCRIPTS_LOCK), \ cp -a $(DEPS_DIR)/rabbit/scripts/* $(CLI_SCRIPTS_DIR)/) + $(verbose) \ + $(foreach cmd, $(LEGACY_CLI_COMMANDS), \ + ln -sf rabbitmq.escript $(CLI_SCRIPTS_DIR)/$(cmd).escript;) + $(verbose) \ + $(foreach cmd, $(LEGACY_CLI_COMMANDS), \ + ln -sf rabbitmq $(CLI_SCRIPTS_DIR)/$(cmd);) install-cli-escripts: | $(CLI_ESCRIPTS_DIR) $(gen_verbose) $(call maybe_flock,$(CLI_ESCRIPTS_LOCK), \ pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy