#!/usr/bin/env bash # -*- tab-width:4;indent-tabs-mode:nil -*- # ex: ts=4 sw=4 et set -euo pipefail DEBUG="${DEBUG:-0}" if [ "$DEBUG" -eq 1 ]; then set -x fi if [ "$DEBUG" -eq 2 ]; then set -x export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' fi # We need to find real directory with emqx files on all platforms # even when bin/emqx is symlinked on several levels # - readlink -f works perfectly, but `-f` flag has completely different meaning in BSD version, # so we can't use it universally. # - `stat -f%R` on MacOS does exactly what `readlink -f` does on Linux, but we can't use it # as a universal solution either because GNU stat has different syntax and this argument is invalid. # Also, version of stat which supports this syntax is only available since MacOS 12 if [ "$(uname -s)" == 'Darwin' ]; then product_version="$(sw_vers -productVersion | cut -d '.' -f 1)" if [ "$product_version" -ge 12 ]; then # if homebrew coreutils package is installed, GNU version of stat can take precedence, # so we use absolute path to ensure we are calling MacOS default RUNNER_ROOT_DIR="$(cd "$(dirname "$(/usr/bin/stat -f%R "$0" || echo "$0")")"/..; pwd -P)" else # try our best to resolve link on MacOS <= 11 RUNNER_ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)" fi else RUNNER_ROOT_DIR="$(cd "$(dirname "$(realpath "$0" || echo "$0")")"/..; pwd -P)" fi # shellcheck disable=SC1090,SC1091 . "$RUNNER_ROOT_DIR"/releases/emqx_vars # defined in emqx_vars export RUNNER_ROOT_DIR export EMQX_ETC_DIR export REL_VSN export SCHEMA_MOD export IS_ENTERPRISE RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME" CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN" WHOAMI=$(whoami) # hocon try to read environment variables starting with "EMQX_" export HOCON_ENV_OVERRIDE_PREFIX='EMQX_' export ERTS_DIR="$RUNNER_ROOT_DIR/erts-$ERTS_VSN" export BINDIR="$ERTS_DIR/bin" export EMU="beam" export PROGNAME="erl" export ERTS_LIB_DIR="$RUNNER_ROOT_DIR/lib" DYNLIBS_DIR="$RUNNER_ROOT_DIR/dynlibs" logerr() { if [ "${TERM:-dumb}" = dumb ]; then echo -e "ERROR: $*" 1>&2 else echo -e "$(tput setaf 1)ERROR: $*$(tput sgr0)" 1>&2 fi } logwarn() { if [ "${TERM:-dumb}" = dumb ]; then echo "WARNING: $*" else echo "$(tput setaf 3)WARNING: $*$(tput sgr0)" fi } logdebug() { if [ "$DEBUG" -eq 1 ]; then echo "DEBUG: $*" fi } die() { set +x logerr "$1" errno=${2:-1} exit "$errno" } assert_node_alive() { if ! relx_nodetool "ping" > /dev/null; then exit 1 fi } usage() { local command="$1" case "$command" in start) echo "Start EMQX service in daemon mode" ;; stop) echo "Stop the running EMQX program" ;; console) echo "Boot up EMQX service in an interactive Erlang or Elixir shell" echo "This command needs a tty" ;; console_clean) echo "This command does NOT boot up the EMQX service" echo "It only starts an interactive Erlang or Elixir console with all the" echo "EMQX code available" ;; foreground) echo "Start EMQX in foreground mode without an interactive shell" ;; pid) echo "Print out EMQX process identifier" ;; ping) echo "Check if the EMQX node is up and running" echo "This command exit with 0 silently if node is running" ;; escript) echo "Execute a escript using the Erlang runtime from EMQX package installation" echo "For example $REL_NAME escript /path/to/my/escript my_arg1 my_arg2" ;; attach) echo "This command is applicable when EMQX is started in daemon mode." echo "It attaches the current shell to EMQX's control console" echo "through a named pipe." logwarn "try to use the safer alternative, remote_console command." ;; remote_console) echo "Start an interactive shell running an Erlang or Elixir node which " echo "hidden-connects to the running EMQX node". echo "This command is mostly used for troubleshooting." ;; ertspath) echo "Print path to Erlang runtime bin dir" ;; rpc) echo "Usage: $REL_NAME rpc MODULE FUNCTION [ARGS, ...]" echo "Connect to the EMQX node and make an Erlang RPC" echo "This command blocks for at most 60 seconds." echo "It exits with non-zero code in case of any RPC failure" echo "including connection error and runtime exception" ;; rpcterms) echo "Usage: $REL_NAME rpcterms MODULE FUNCTION [ARGS, ...]" echo "Connect to the EMQX node and make an Erlang RPC" echo "The result of the RPC call is pretty-printed as an " echo "Erlang term" ;; root_dir) echo "Print EMQX installation root dir" ;; eval) echo "Evaluate an Erlang expression in the EMQX node." ;; eval-ex) echo "Evaluate an Elixir expression in the EMQX node. Only applies to Elixir node" ;; versions) echo "List installed EMQX release versions and their status" ;; unpack) echo "Usage: $REL_NAME unpack [VERSION]" echo "Unpacks a release package VERSION, it assumes that this" echo "release package tarball has already been deployed at one" echo "of the following locations:" echo " releases/-.tar.gz" ;; install) echo "Usage: $REL_NAME install [VERSION]" echo "Installs a release package VERSION, it assumes that this" echo "release package tarball has already been deployed at one" echo "of the following locations:" echo " releases/-.tar.gz" echo "" echo " --no-permanent Install release package VERSION but" echo " don't make it permanent" ;; uninstall) echo "Usage: $REL_NAME uninstall [VERSION]" echo "Uninstalls a release VERSION, it will only accept" echo "versions that are not currently in use" ;; upgrade) echo "Usage: $REL_NAME upgrade [VERSION]" echo "Upgrades the currently running release to VERSION, it assumes" echo "that a release package tarball has already been deployed at one" echo "of the following locations:" echo " releases/-.tar.gz" echo "" echo " --no-permanent Install release package VERSION but" echo " don't make it permanent" ;; downgrade) echo "Usage: $REL_NAME downgrade [VERSION]" echo "Downgrades the currently running release to VERSION, it assumes" echo "that a release package tarball has already been deployed at one" echo "of the following locations:" echo " releases/-.tar.gz" echo "" echo " --no-permanent Install release package VERSION but" echo " don't make it permanent" ;; check_config) echo "Checks the EMQX config without generating any files" ;; *) echo "Usage: $REL_NAME COMMAND [help]" echo '' echo "Commonly used COMMANDs:" echo " start: Start EMQX in daemon mode" echo " console: Start EMQX in an interactive Erlang or Elixir shell" echo " foreground: Start EMQX in foreground mode without an interactive shell" echo " stop: Stop the running EMQX node" echo " ctl: Administration commands, execute '$REL_NAME ctl help' for more details" echo '' echo "More:" echo " Shell attach: remote_console | attach" # echo " Up/Down-grade: upgrade | downgrade | install | uninstall | versions" # TODO enable when supported echo " Install Info: ertspath | root_dir" echo " Runtime Status: pid | ping" echo " Validate Config: check_config" echo " Advanced: console_clean | escript | rpc | rpcterms | eval | eval-ex" echo '' echo "Execute '$REL_NAME COMMAND help' for more information" ;; esac } COMMAND="${1:-}" GREP='grep --color=never' if [ -z "$COMMAND" ]; then usage 'help' exit 1 elif [ "$COMMAND" = 'help' ]; then usage 'help' exit 0 fi if [ "${2:-}" = 'help' ]; then ## 'ctl' command has its own usage info if [ "$COMMAND" != 'ctl' ]; then usage "$COMMAND" exit 0 fi fi ## IS_BOOT_COMMAND is set for later to inspect node name and cookie from hocon config (or env variable) case "${COMMAND}" in start|console|console_clean|foreground|check_config) IS_BOOT_COMMAND='yes' ;; ertspath) echo "$ERTS_DIR" exit 0 ;; root_dir) echo "$RUNNER_ROOT_DIR" exit 0 ;; *) IS_BOOT_COMMAND='no' ;; esac ## backward compatible if [ -d "$ERTS_DIR/lib" ]; then export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH" fi # Simple way to check the correct user and fail early check_user() { # Validate that the user running the script is the owner of the # RUN_DIR. if [ "$RUNNER_USER" ] && [ "x$WHOAMI" != "x$RUNNER_USER" ]; then if [ "x$WHOAMI" != "xroot" ]; then echo "You need to be root or use sudo to run this command" exit 1 fi CMD="DEBUG=$DEBUG \"$RUNNER_SCRIPT\" " for ARG in "$@"; do CMD="${CMD} \"$ARG\"" done # This will drop privileges into the runner user # It exec's in a new shell and the current shell will exit exec su - "$RUNNER_USER" -c "$CMD" fi } # Make sure the user running this script is the owner and/or su to that user check_user "$@" ES=$? if [ "$ES" -ne 0 ]; then exit $ES fi # Make sure log directory exists mkdir -p "$EMQX_LOG_DIR" # turn off debug as this is static set +x COMPATIBILITY_CHECK=' io:format("BEAM_OK~n", []), try [_|_] = L = crypto:info_lib(), io:format("CRYPTO_OK ~0p~n", [L]) catch _ : _ -> %% so logger has the chance to log something timer:sleep(100), halt(1) end, try mnesia_hook:module_info(), io:format("MNESIA_OK~n", []) catch _ : _ -> io:format("WARNING: Mnesia app has no post-coommit hook support~n", []), halt(2) end, halt(0). ' [ "$DEBUG" -eq 1 ] && set -x compatiblity_info() { # RELEASE_LIB is used by Elixir # set crash-dump bytes to zero to ensure no crash dump is generated when erl crashes env ERL_CRASH_DUMP_BYTES=0 "$BINDIR/$PROGNAME" \ -noshell \ -boot "$REL_DIR/start_clean" \ -boot_var RELEASE_LIB "$ERTS_LIB_DIR/lib" \ -eval "$COMPATIBILITY_CHECK" } # Collect Erlang/OTP runtime sanity and compatibility in one go maybe_use_portable_dynlibs() { # Read BUILD_INFO early as the next commands may mess up the shell BUILD_INFO="$(cat "${REL_DIR}/BUILD_INFO")" COMPATIBILITY_INFO="$(compatiblity_info 2>/dev/null || true)" if ! (echo -e "$COMPATIBILITY_INFO" | $GREP -q 'CRYPTO_OK'); then ## failed to start, might be due to missing libs, try to be portable export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-$DYNLIBS_DIR}" if [ "$LD_LIBRARY_PATH" != "$DYNLIBS_DIR" ]; then export LD_LIBRARY_PATH="$DYNLIBS_DIR:$LD_LIBRARY_PATH" fi ## Turn off debug, because COMPATIBILITY_INFO needs to capture stderr COMPATIBILITY_INFO="$(compatiblity_info 2>&1 || true)" if ! (echo -e "$COMPATIBILITY_INFO" | $GREP -q 'BEAM_OK'); then ## not able to start beam.smp logerr "$COMPATIBILITY_INFO" logerr "Please ensure it is running on the correct platform:" logerr "$BUILD_INFO" logerr "Version=$REL_VSN" logerr "Required dependencies: openssl-1.1.1 (libcrypto), libncurses and libatomic1" exit 1 elif ! (echo -e "$COMPATIBILITY_INFO" | $GREP -q 'CRYPTO_OK'); then ## not able to start crypto app logerr "$COMPATIBILITY_INFO" exit 2 fi logwarn "Using libs from '${DYNLIBS_DIR}' due to missing from the OS." fi } SED_REPLACE="sed -i " case $(sed --help 2>&1) in *GNU*) SED_REPLACE="sed -i ";; *BusyBox*) SED_REPLACE="sed -i ";; *) SED_REPLACE="sed -i '' ";; esac # Get node pid relx_get_pid() { if output="$(relx_nodetool rpcterms os getpid)" then # shellcheck disable=SC2001 # Escaped quote taken as closing quote in editor echo "$output" | sed -e 's/"//g' return 0 else echo "$output" return 1 fi } # Connect to a remote node remsh() { # Generate a unique id used to allow multiple remsh to the same node # transparently id="remsh$(gen_node_id)-${NAME}" # shellcheck disable=SC2086 # Setup remote shell command to control node if [ "$IS_ELIXIR" = no ] || [ "${EMQX_CONSOLE_FLAVOR:-}" = 'erl' ] ; then set -- "$BINDIR/erl" "$NAME_TYPE" "$id" \ -remsh "$NAME" -boot "$REL_DIR/start_clean" \ -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ -boot_var RELEASE_LIB "$ERTS_LIB_DIR" \ -setcookie "$COOKIE" \ -hidden \ -kernel net_ticktime "$TICKTIME" \ $EPMD_ARGS else set -- "$REL_DIR/iex" \ --remsh "$NAME" \ --boot-var RELEASE_LIB "$ERTS_LIB_DIR" \ --cookie "$COOKIE" \ --hidden \ --erl "-kernel net_ticktime $TICKTIME" \ --erl "$EPMD_ARGS" \ --erl "$NAME_TYPE $id" \ --boot "$REL_DIR/start_clean" fi exec "$@" } # Generate a random id gen_node_id() { od -t u -N 4 /dev/urandom | head -n1 | awk '{print $2 % 1000}' } call_nodetool() { "$ERTS_DIR/bin/escript" "$RUNNER_ROOT_DIR/bin/nodetool" "$@" } # Control a node relx_nodetool() { command="$1"; shift ERL_FLAGS="${ERL_FLAGS:-} $EPMD_ARGS -setcookie $COOKIE" \ call_nodetool "$NAME_TYPE" "$NAME" "$command" "$@" } call_hocon() { call_nodetool hocon "$@" \ || die "call_hocon_failed: $*" $? } find_emqx_process() { ## Find the running node from 'ps -ef' ## * The grep args like '[e]mqx' but not 'emqx' is to avoid greping the grep command itself ## * The running 'remsh' and 'nodetool' processes must be excluded if [ -n "${EMQX_NODE__NAME:-}" ]; then # if node name is provided, filter by node name # shellcheck disable=SC2009 ps -ef | $GREP '[e]mqx' | $GREP -v -E '(remsh|nodetool)' | $GREP -E "\s-s?name\s${EMQX_NODE__NAME}" | $GREP -oE "\-[r]oot ${RUNNER_ROOT_DIR}.*" || true else # shellcheck disable=SC2009 ps -ef | $GREP '[e]mqx' | $GREP -v -E '(remsh|nodetool)' | $GREP -oE "\-[r]oot ${RUNNER_ROOT_DIR}.*" || true fi } ## Resolve boot configs in a batch ## This is because starting the Erlang beam with all modules loaded ## and parsing HOCON config + environment variables is a non-trivial task CONF_KEYS=( 'node.data_dir' 'node.name' 'node.cookie' 'node.db_backend' 'cluster.proto_dist' 'node.dist_net_ticktime' ) if [ "$IS_ENTERPRISE" = 'yes' ]; then CONF_KEYS+=( 'license.key' ) fi ## To be backward compatible, read and then unset EMQX_NODE_NAME if [ -n "${EMQX_NODE_NAME:-}" ]; then export EMQX_NODE__NAME="${EMQX_NODE_NAME}" unset EMQX_NODE_NAME fi # Turn off debug as the ps output can be quite noisy set +x PS_LINE="$(find_emqx_process)" logdebug "PS_LINE=$PS_LINE" RUNNING_NODES_COUNT="$(echo -e "$PS_LINE" | sed '/^\s*$/d' | wc -l)" [ "$RUNNING_NODES_COUNT" -gt 1 ] && logdebug "More than one running node found: count=$RUNNING_NODES_COUNT" if [ "$IS_BOOT_COMMAND" = 'yes' ]; then if [ "$RUNNING_NODES_COUNT" -gt 0 ] && [ "$COMMAND" != 'check_config' ]; then running_node_name=$(echo -e "$PS_LINE" | $GREP -oE "\s-s?name.*" | awk '{print $2}' || true) if [ -n "$running_node_name" ] && [ "$running_node_name" = "${EMQX_NODE__NAME:-}" ]; then echo "Node ${running_node_name} is already running!" exit 1 fi fi [ -f "$EMQX_ETC_DIR"/emqx.conf ] || die "emqx.conf is not found in $EMQX_ETC_DIR" 1 maybe_use_portable_dynlibs if [ "${EMQX_BOOT_CONFIGS:-}" = '' ]; then EMQX_BOOT_CONFIGS="$(call_hocon -s "$SCHEMA_MOD" -c "$EMQX_ETC_DIR"/emqx.conf multi_get "${CONF_KEYS[@]}")" ## export here so the 'console' command recursively called from ## 'start' command does not have to parse the configs again export EMQX_BOOT_CONFIGS fi else # For non-boot commands, we need below runtime facts to connect to the running node: # 1. The running node name; # 2. The Erlang cookie in use by the running node name; # 3. SSL options if the node is using TLS for Erlang distribution; # 4. Erlang kernel application's net_ticktime config. # # There are 3 sources of truth to get those runtime information. # Listed in the order of preference: # 1. The boot command (which can be inspected from 'ps -ef' command output) # 2. The generated vm.