#!/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

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
}

loginfo() {
    if [ "${TERM:-dumb}" = dumb ]; then
        echo "INFO: $*"
    else
        echo "$(tput setaf 2)INFO: $*$(tput sgr0)"
    fi
}

logdebug() {
    if [ "$DEBUG" -eq 1 ]; then
        echo "DEBUG: $*"
    fi
}

die() {
    set +x
    logerr "$1"
    errno=${2:-1}
    exit "$errno"
}

# Print the given arguments space-joined, with the value following
# any -setcookie/--cookie flag masked.
redact_cookie_args() {
    local prev='' tok out=''
    for tok in "$@"; do
        case "$prev" in
            -setcookie|--cookie) out="${out}****** " ;;
            *) out="${out}${tok} " ;;
        esac
        prev="$tok"
    done
    printf '%s' "${out% }"
}

THIS_SCRIPT="$0"

usage() {
    local command="$1"
    local progname="$THIS_SCRIPT"

    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"
        echo "Passing it the --iex flag will run an IEx (Elixir console)"
        ;;
    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"
        echo "Passing it the --iex flag will run an IEx (Elixir console)"
        ;;
    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 $progname 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|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."
        echo "Passing it the --iex flag will run an IEx (Elixir console)"
        ;;
    ertspath)
        echo "Print path to Erlang runtime bin dir"
        ;;
    rpc)
        echo "Usage: $progname 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: $progname 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."
        echo "Warning: When audit logging is enabled, the expression is logged without redaction."
        ;;
    eval-ex)
        echo "Evaluate an Elixir expression in the EMQX node. Only applies to Elixir node"
        ;;
    check_config)
        echo "Checks the EMQX config without generating any files"
        ;;
    *)
        echo "Usage: $progname COMMAND [help]"
        echo ''
        echo "Commonly used COMMANDs:"
        echo "  start:            Start EMQX in daemon mode"
        echo "  console [--iex]:  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 '$progname ctl help' for more details"
        echo ''
        echo "More:"
        echo "  Shell attach:     remote [--iex] | attach"
        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 '$progname COMMAND help' for more information"
    ;;
    esac
}

# 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 "$THIS_SCRIPT" || echo "$THIS_SCRIPT")")"/..; pwd -P)"
    else
        # try our best to resolve link on MacOS <= 11
        RUNNER_ROOT_DIR="$(cd "$(dirname "$(readlink "$THIS_SCRIPT" || echo "$THIS_SCRIPT")")"/..; pwd -P)"
    fi
else
    RUNNER_ROOT_DIR="$(cd "$(dirname "$(realpath "$THIS_SCRIPT" || echo "$THIS_SCRIPT")")"/..; pwd -P)"
fi

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

RELUP_DIR="relup"
BASE_RUNNER_ROOT_DIR="${BASE_RUNNER_ROOT_DIR:-$RUNNER_ROOT_DIR}"
RELUP_PATH="$RUNNER_ROOT_DIR/$RELUP_DIR"

if [ -f "$RELUP_PATH/current" ]; then
    TARGET_VSN=$(cat "$RELUP_PATH/current")
    export BASE_RUNNER_ROOT_DIR
    ## only print for boot commands to avoid messing the CLI outputs
    if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
        loginfo "Loading emqx from hot-upgrade dir: $RELUP_PATH"
    fi
    exec "$RELUP_PATH/$TARGET_VSN"/bin/emqx "$@"
fi

# Turn off debug: the sourced file may set secrets such as
# EMQX_NODE_COOKIE, which must not be traced by 'set -x'.
set +x
# 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

## Boot-time environment variables (EMQX_FEATURES, EMQX_SECURITY_PROFILE, ...).
## The file is optional. Values in the file override the inherited environment.
if [ -f "$EMQX_ETC_DIR/emqx.env" ]; then
    set -a
    # shellcheck disable=SC1090,SC1091
    . "$EMQX_ETC_DIR/emqx.env"
    set +a
fi
[[ "$DEBUG" -gt 0 ]] && set -x

## starts only the slim apps list, and we flip code loading to interactive so
## the skipped apps' .beam files don't sit resident in memory.
if [ "${EMQX_FEATURES:-}" = 'ESSENTIAL' ]; then
    export CODE_LOADING_MODE="${CODE_LOADING_MODE:-interactive}"
fi

# Will be set to hardened since v7
export EMQX_SECURITY_PROFILE="${EMQX_SECURITY_PROFILE:-legacy}"

if [[ "$IS_BOOT_COMMAND" = 'yes' ]]; then
    if [ "$EMQX_SECURITY_PROFILE" != 'legacy' ] && [ "$EMQX_SECURITY_PROFILE" != 'hardened' ]; then
        logerr "Invalid security profile: $EMQX_SECURITY_PROFILE. Valid values are: legacy, hardened."
        exit 1
    fi
fi


RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME"
CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}"
REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN"

WHOAMI=$(whoami 2>/dev/null || id -u)

# 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"

assert_node_alive() {
    if ! relx_nodetool "ping" > /dev/null; then
        exit 1
    fi
}

## 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(module),
        io:format("MNESIA_OK~n", [])
    catch
        _ : _ ->
            io:format("WARNING: Mnesia app has no post-coommit hook support~n", []),
            halt(2)
    end,
    halt(0).
'
[[ "$DEBUG" -gt 0 ]] && 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 \
    +S 2 \
    +P 65536 \
    +Q 65536 \
    -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}"

    # Turn off debug (and keep it off through the exec below) so the
    # cookie in the command arguments isn't traced by 'set -x'.
    set +x

    # shellcheck disable=SC2086
    # Setup remote shell command to control node
    if [ "${EMQX_CONSOLE_FLAVOR:-erl}" = '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" \
            -kernel standard_io_encoding unicode \
            +P 65536 \
            +Q 65536 \
            +S 2 \
            $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 "-kernel standard_io_encoding unicode" \
            --erl "$EPMD_ARGS" \
            --erl "$NAME_TYPE $id" \
            --erl "+P 65536" \
            --erl "+Q 65536" \
            --erl "+S 2" \
            --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() {
    ## Emulator and -kernel flags come from the %%! line in bin/nodetool.
    "$ERTS_DIR/bin/escript" "$RUNNER_ROOT_DIR/bin/nodetool" "$@"
}

# Control a node
relx_nodetool() {
    local ret=0
    command="$1"; shift
    # Turn off debug so the cookie value isn't traced by 'set -x'.
    set +x
    ERL_FLAGS="${ERL_FLAGS:-} $EPMD_ARGS -setcookie $COOKIE" \
             call_nodetool "$NAME_TYPE" "$NAME" "$command" "$@" || ret=$?
    if [ "$DEBUG" -gt 0 ]; then set -x; fi
    return $ret
}

call_hocon() {
    call_nodetool hocon "$@" \
        || die "call_hocon_failed: $*" $?
}

check_emqx_process() {
    local rootdir="$1"
    ## 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
    ps -efww | $GREP '[e]mqx' | $GREP -v -E '(remsh|nodetool)' | $GREP -oE "\-[r]oot ${rootdir}.*" || true
}

find_emqx_process() {
    ## Maybe the emqx has been hot upgraded and is still running from the base root_dir.
    ## So instead of searching RUNNER_ROOT_DIR, we only search for processes running
    ## from BASE_RUNNER_ROOT_DIR (which is either equal to RUNNER_ROOT_DIR or a
    ## parent directory of it).
    local rootdir="${BASE_RUNNER_ROOT_DIR}"
    if [ -n "${EMQX_NODE__NAME:-}" ]; then
        # if node name is provided, filter by node name
        check_emqx_process "${rootdir}" | $GREP -E "\s-s?name\s${EMQX_NODE__NAME}" || true
    else
        check_emqx_process "${rootdir}"
    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: the cookie value must not be traced by 'set -x',
# and the ps output below can be quite noisy.
set +x

## Resolve Erlang cookie.
if [ -n "${EMQX_NODE_COOKIE:-}" ]; then
    ## To be backward compatible, read and unset EMQX_NODE_COOKIE
    export EMQX_NODE__COOKIE="${EMQX_NODE_COOKIE}"
    unset EMQX_NODE_COOKIE
fi

PS_LINE="$(find_emqx_process)"
logdebug "PS_LINE=$(printf '%s' "$PS_LINE" | sed -E 's/(-setcookie) +[^ ]+/\1 ******/g')"
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.<time>.config file located in the dir pointed by 'node.data_dir'
    #  3. The bootstrap config 'etc/emqx.conf'
    #
    # If failed to read from source 1, the information is retrieved from source 3
    # i.e. source 2 is never used.
    #
    # NOTES:
    #  * We should avoid getting runtime information with the 3rd approach because 'etc/emqx.conf' might
    #    be updated after the node is started. e.g. If a user starts the node with name 'emqx@127.0.0.1'
    #    then update the config in the file to 'node.name = "emqx@local.net"', after this change,
    #    there would be no way stop the running node 'emqx@127.0.0.1', because 'emqx stop' command
    #    would try to stop the new node instead.
    if [ "$RUNNING_NODES_COUNT" -eq 1 ]; then
        ## only one emqx node is running, get running args from 'ps -ef' output
        tmp_nodename=$(echo -e "$PS_LINE" | $GREP -oE "\s-s?name.*" | awk '{print $2}' || true)
        tmp_cookie=$(echo -e "$PS_LINE" | $GREP -oE "\s-setcookie.*" | awk '{print $2}' || true)
        tmp_proto_dist=$(echo -e "$PS_LINE" | $GREP -oE '\s-ekka_proto_dist.*' | awk '{print $2}' || echo 'inet_tcp')
        SSL_DIST_OPTFILE="$(echo -e "$PS_LINE" | $GREP -oE '\-ssl_dist_optfile\s.+\s' | awk '{print $2}' || true)"
        tmp_ticktime="$(echo -e "$PS_LINE" | $GREP -oE '\s-kernel\snet_ticktime\s.+\s' | awk '{print $3}' || true)"
        tmp_datadir="$(echo -e "$PS_LINE" | $GREP -oE "\-emqx_data_dir.*" | sed -E 's#.+emqx_data_dir[[:blank:]]##g' | sed -E 's#[[:blank:]]--$##g' || true)"
        ## Make the format like what call_hocon multi_get prints out, but only need 4 args
        EMQX_BOOT_CONFIGS="node.name=${tmp_nodename}\nnode.cookie=${tmp_cookie}\ncluster.proto_dist=${tmp_proto_dist}\nnode.dist_net_ticktime=$tmp_ticktime\nnode.data_dir=${tmp_datadir}"
    else
        if [ "$RUNNING_NODES_COUNT" -gt 1 ]; then
            if [ -z "${EMQX_NODE__NAME:-}" ]; then
                tmp_nodenames=$(echo -e "$PS_LINE" | $GREP -oE "\s-s?name.*" | awk '{print $2}' | tr '\n' ' ')
                logerr "More than one EMQX node found running (root dir: ${RUNNER_ROOT_DIR})"
                logerr "Running nodes: $tmp_nodenames"
                logerr "Make sure environment variable EMQX_NODE__NAME is set to indicate for which node this command is intended."
                exit 1
            fi
        else
            if [ -n "${EMQX_NODE__NAME:-}" ]; then
                die "Node $EMQX_NODE__NAME is not running?"
            fi
        fi
        ## We have no choice but to read the bootstrap config (with environment overrides available in the current shell)
        [ -f "$EMQX_ETC_DIR"/emqx.conf ] || die "emqx.conf is not found in $EMQX_ETC_DIR" 1
        maybe_use_portable_dynlibs
        EMQX_BOOT_CONFIGS="$(call_hocon -s "$SCHEMA_MOD" -c "$EMQX_ETC_DIR"/emqx.conf multi_get "${CONF_KEYS[@]}")"
    fi
fi
# Redact the raw serialized string before any escape expansion, so an
# embedded escape cannot split a secret across lines. Values may be
# quoted (from hocon multi_get) or bare (parsed from ps output).
logdebug "EMQX_BOOT_CONFIGS: $(printf '%s' "$EMQX_BOOT_CONFIGS" | sed -E 's/(node\.cookie|license\.key)=("(\\.|[^"\\])*"|[^\\ ]*)/\1=******/g')"
[[ "$DEBUG" -gt 0 ]] && set -x

# Subshell body with trace off: EMQX_BOOT_CONFIGS may contain secrets
# (node.cookie, license.key) which must not be traced by 'set -x'.
get_boot_config() (
    set +x
    path_to_value="$1"
    echo -e "$EMQX_BOOT_CONFIGS" | $GREP "$path_to_value=" | sed -e "s/$path_to_value=//g" | tr -d \"
)

EPMD_ARGS="${EPMD_ARGS:-"-start_epmd false -epmd_module ekka_epmd -proto_dist ekka"}"
PROTO_DIST="$(get_boot_config 'cluster.proto_dist' || true)"
TICKTIME="$(get_boot_config 'node.dist_net_ticktime' || echo '60')"
# this environment variable is required by ekka_dist module
# because proto_dist is overriden to ekka, and there is a lack of ekka_tls module
export EKKA_PROTO_DIST_MOD="${PROTO_DIST:-inet_tcp}"
if [ "$EKKA_PROTO_DIST_MOD" = 'inet_tls' ] || [ "$EKKA_PROTO_DIST_MOD" = 'inet6_tls' ]; then
    if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
        SSL_DIST_OPTFILE=${EMQX_SSL_DIST_OPTFILE:-"$EMQX_ETC_DIR/ssl_dist.conf"}
        case "$SSL_DIST_OPTFILE" in
            *\ *)
                # there is unfortunately no way to support space for this option because we'd need to grep
                # from 'ps -ef' result to get this option for non-boot commands (nodetool) to run
                set +x
                logerr "Got space in: $SSL_DIST_OPTFILE"
                logerr "No space is allowed for Erlang distribution over SSL option file path."
                logerr "Configure it from environment variable EMQX_SSL_DIST_OPTFILE."
                logerr "Or make sure emqx root path '$RUNNER_ROOT_DIR' has no space"
                exit 1
                ;;
            *)
                true
                ;;
        esac
    fi
    EPMD_ARGS="${EPMD_ARGS} -ssl_dist_optfile $SSL_DIST_OPTFILE"
fi

DATA_DIR="$(get_boot_config 'node.data_dir')"
# ensure no trailing /
DATA_DIR="${DATA_DIR%/}"
if [[ $DATA_DIR != /* ]]; then
    # relative path
    DATA_DIR="${BASE_RUNNER_ROOT_DIR}/${DATA_DIR}"
fi
CONFIGS_DIR="$DATA_DIR/configs"
mkdir -p "$CONFIGS_DIR"

check_license() {
    if [ "$IS_ENTERPRISE" == "no" ]; then
        return 0
    fi

    # Turn off debug so the license key isn't traced by 'set -x'.
    set +x
    key_license="${EMQX_LICENSE__KEY:-$(get_boot_config 'license.key' || echo '')}"

    if [[ -n "$key_license" && ("$key_license" != "undefined") ]]; then
      local ret=0
      call_nodetool check_license_key "$key_license" || ret=$?
      if [ "$DEBUG" -gt 0 ]; then set -x; fi
      return $ret
    else
      set +x
      logerr "License not found."
      logerr "Please specify one via the EMQX_LICENSE__KEY variable"
      logerr "or via license.key in emqx.conf."
      return 1
    fi
}

# Run an escript in the node's environment
relx_escript() {
    shift; scriptpath="$1"; shift
    ERL_FLAGS="${ERL_FLAGS:-} -kernel standard_io_encoding unicode" \
        "$ERTS_DIR/bin/escript" "$RUNNER_ROOT_DIR/$scriptpath" "$@"
}

# Output a start command for the last argument of run_erl
relx_start_command() {
    printf "exec \"%s\" \"%s\"" "$RUNNER_SCRIPT" \
           "$START_OPTION"
}

# Function to check configs without generating them
check_config() {
    ## this command checks the configs without generating any files
    call_hocon -v \
        -s "$SCHEMA_MOD" \
        -c "$EMQX_ETC_DIR"/base.hocon \
        -c "$CONFIGS_DIR"/cluster.hocon \
        -c "$EMQX_ETC_DIR"/emqx.conf \
        check_schema
}

# Function to generate app.config and vm.args
# sets two environment variables CONF_FILE and ARGS_FILE
generate_config() {
    local name_type="$1"
    local node_name="$2"
    ## Delete the *.siz files first or it can't start after
    ## changing the config 'log.rotation.size'
    rm -f "${EMQX_LOG_DIR}"/*.siz

    ## timestamp for each generation
    local NOW_TIME
    NOW_TIME="$(date +'%Y.%m.%d.%H.%M.%S')"

    ## This command populates two files: app.<time>.config and vm.<time>.args
    ## It takes input sources and overlays values in below order:
    ##   - etc/base.hocon
    ##   - $CONFIGS_DIR/cluster.hocon
    ##   - etc/emqx.conf
    ##   - environment variables starts with EMQX_ e.g. EMQX_NODE__ROLE
    ##
    ## NOTE: it's a known issue that cluster.hocon may change right after the node boots up
    ##       because it has to sync cluster.hocon from other nodes.
    call_hocon -v -t "$NOW_TIME" \
        -s "$SCHEMA_MOD" \
        -c "$EMQX_ETC_DIR"/base.hocon \
        -c "$CONFIGS_DIR"/cluster.hocon \
        -c "$EMQX_ETC_DIR"/emqx.conf \
        -d "$DATA_DIR"/configs generate

    ## filenames are per-hocon convention
    CONF_FILE="$CONFIGS_DIR/app.$NOW_TIME.config"
    ARGS_FILE="$CONFIGS_DIR/vm.$NOW_TIME.args"

    ## Merge hocon generated *.args into the vm.args
    TMP_ARG_FILE="$CONFIGS_DIR/vm.args.tmp"
    cp "$EMQX_ETC_DIR/vm.args" "$TMP_ARG_FILE"
    echo "" >> "$TMP_ARG_FILE"
    echo "-pa \"${REL_DIR}/consolidated\"" >> "$TMP_ARG_FILE"
    ## read lines from generated vm.<time>.args file
    ## drop comment lines, and empty lines using sed
    ## pipe the lines to a while loop
    ## Turn off debug: the generated args contain the '-setcookie' line,
    ## which must not be traced by 'set -x'.
    set +x
    sed '/^#/d' "$ARGS_FILE" | sed '/^$/d' | while IFS='' read -r ARG_LINE || [ -n "$ARG_LINE" ]; do
        ## in the loop, split the 'key[:space:]value' pair
        ARG_KEY=$(echo "$ARG_LINE" | awk '{$NF="";print}')
        ARG_VALUE=$(echo "$ARG_LINE" | awk '{print $NF}')
        ## use the key to look up in vm.args file for the value
        TMP_ARG_VALUE=$($GREP "^$ARG_KEY" "$TMP_ARG_FILE" || true | awk '{print $NF}')
        ## compare generated (to override) value to original (to be overridden) value
        if [ "$ARG_VALUE" != "$TMP_ARG_VALUE" ] ; then
            ## if they are different
            if [ -n "$TMP_ARG_VALUE" ]; then
                ## if the old value is present, replace it with generated value
                sh -c "$SED_REPLACE 's|^$ARG_KEY.*$|$ARG_LINE|' \"$TMP_ARG_FILE\""
            else
                ## otherwise append generated value to the end
                echo "$ARG_LINE" >> "$TMP_ARG_FILE"
            fi
        fi
    done
    if [ "$DEBUG" -gt 0 ]; then set -x; fi
    echo "$name_type $node_name" >> "$TMP_ARG_FILE"
    echo "-mnesia dir '\"$DATA_DIR/mnesia/$NAME\"'" >> "$TMP_ARG_FILE"
    ## rename the generated vm.<time>.args file
    mv -f "$TMP_ARG_FILE" "$ARGS_FILE"
}

# check if a PID is defunct
# shellcheck disable=SC2317 # call in func `is_down()`
is_defunct() {
    local PID="$1"
    ps -fp "$PID" | $GREP -q 'defunct'
}

# check if a PID is down
# shellcheck disable=SC2317 # call in func `nodetool_shutdown()`
is_down() {
    PID="$1"
    if ps -p "$PID" >/dev/null; then
        # still around
        # shellcheck disable=SC2009 # this grep pattern is not a part of the program names
        if is_defunct "$PID"; then
            # zombie state, print parent pid
            parent="$(ps -o ppid= -p "$PID" | tr -d ' ')"
            if [ -z "$parent" ] && ! is_defunct "$PID"; then
                # process terminated in the meanwhile
                return 0;
            fi
            logwarn "$PID is marked <defunct>, parent: $(ps -p "$parent")"
            return 0
        fi
        return 1
    fi
    # it's gone
    return 0
}

wait_for() {
    local WAIT_TIME
    local CMD
    WAIT_TIME="$1"
    shift
    CMD="$*"
    while true; do
        if $CMD; then
            return 0
        fi
        if [ "$WAIT_TIME" -le 0 ]; then
            return 1
        fi
        WAIT_TIME=$((WAIT_TIME - 1))
        sleep 1
    done
}

wait_until_return_val() {
    local RESULT
    local WAIT_TIME
    local CMD
    RESULT="$1"
    WAIT_TIME="$2"
    shift 2
    CMD="$*"
    while true; do
        if [ "$($CMD 2>/dev/null)" = "$RESULT" ]; then
            return 0
        fi
        if [ "$WAIT_TIME" -le 0 ]; then
            return 1
        fi
        WAIT_TIME=$((WAIT_TIME - 1))
        sleep 1
    done
}

# First, there is EMQX_DEFAULT_LOG_HANDLER which can control the default values
# to be used when generating configs.
# It's set in docker entrypoint and in systemd service file.
#
# To be backward compatible with 4.x and v5.0.0 ~ v5.0.24/e5.0.2:
# if EMQX_LOG__TO is set, we try to enable handlers from environment variables.
# i.e. it overrides the default value set in EMQX_DEFAULT_LOG_HANDLER
tr_log_to_env() {
    local log_to=${EMQX_LOG__TO:-undefined}
    # unset because it's unknown to 5.0
    unset EMQX_LOG__TO
    case "${log_to}" in
        console)
            export EMQX_LOG__CONSOLE__ENABLE='true'
            export EMQX_LOG__FILE__ENABLE='false'
            ;;
        file)
            export EMQX_LOG__CONSOLE__ENABLE='false'
            export EMQX_LOG__FILE__ENABLE='true'
            ;;
        both)
            export EMQX_LOG__CONSOLE__ENABLE='true'
            export EMQX_LOG__FILE__ENABLE='true'
            ;;
        default)
            # want to use config file defaults, do nothing
            ;;
        undefined)
            # value not set, do nothing
            ;;
        *)
            logerr "Unknown environment value for EMQX_LOG__TO=${log_to} discarded"
            ;;
    esac
}

maybe_log_to_console() {
    if [ "${EMQX_LOG__TO:-}" = 'default' ]; then
        # want to use defaults, do nothing
        unset EMQX_LOG__TO
    else
        tr_log_to_env
        export EMQX_DEFAULT_LOG_HANDLER=${EMQX_DEFAULT_LOG_HANDLER:-console}
    fi
}

# Warn the user if ulimit -n is less than 1024
maybe_warn_ulimit() {
    ULIMIT_F=$(ulimit -n)
    if [ "$ULIMIT_F" -lt 1024 ]; then
        logwarn "ulimit -n is ${ULIMIT_F}; 1024 is the recommended minimum."
    fi
}

## Possible ways to configure emqx node name:
## 1. configure node.name in emqx.conf
## 2. override with environment variable EMQX_NODE__NAME
## Node name is either short-name (without '@'), e.g. 'emqx'
## or long name (with '@') e.g. 'emqx@example.net' or 'emqx@127.0.0.1'
NAME="${EMQX_NODE__NAME:-}"
if [ -z "$NAME" ]; then
    NAME="$(get_boot_config 'node.name')"
fi

# force to use 'emqx' short name
[ -z "$NAME" ] && NAME='emqx'

case "$NAME" in
    *@*)
        NAME_TYPE='-name'
       ;;
    *)
        NAME_TYPE='-sname'
esac
SHORT_NAME="$(echo "$NAME" | awk -F'@' '{print $1}')"
HOST_NAME="$(echo "$NAME" | awk -F'@' '{print $2}')"
if ! (echo "$SHORT_NAME" | $GREP -q '^[0-9A-Za-z_\-]\+$'); then
    logerr "Invalid node name, should be of format '^[0-9A-Za-z_-]+$'."
    exit 1
fi
# This also changes the program name from 'beam.smp' to node name
# e.g. the 'ps' command output
export ESCRIPT_NAME="$SHORT_NAME"

PIPE_DIR="${PIPE_DIR:-/$DATA_DIR/${WHOAMI}_erl_pipes/$NAME/}"

# Resolve a 'file://<path>' cookie source to the secret stored in the file.
# The file may be a regular file or a FIFO (named pipe). A FIFO can be drained
# only once per writer, so this MUST only be called during boot (see below).
resolve_cookie_source() {
    local raw="$1"
    case "$raw" in
        "file://"*)
            local path="${raw#file://}"
            if [ ! -e "$path" ] && [ ! -p "$path" ]; then
                die "node.cookie file '$path' does not exist"
            fi
            local val
            # 'cat' reads both regular files and FIFOs; command substitution
            # strips any trailing newline (e.g. from 'echo secret > file').
            val="$(cat "$path")"
            if [ -z "$val" ]; then
                die "node.cookie file '$path' is empty"
            fi
            # Re-validate the resolved value so we fail with a clear message
            # instead of letting 'erl -setcookie' choke on a weird character.
            case "$val" in
                *[\\\'\"\ ]*)
                    die "resolved node.cookie contains a disallowed character (backslash, single/double quote, or space)"
                    ;;
            esac
            printf '%s' "$val"
            ;;
        *)
            printf '%s' "$raw"
            ;;
    esac
}

# Turn off debug so the cookie value isn't traced by 'set -x' below.
set +x
COOKIE="${EMQX_NODE__COOKIE:-}"
COOKIE_IN_USE="$(get_boot_config 'node.cookie')"
# Tracks whether the cookie was sourced from a 'file://' URL (see below).
COOKIE_FROM_FILE='no'
# Resolve 'file://' cookie sources only during boot. The resolution reads the
# referenced file (possibly a write-once FIFO) and must happen before
# 'erl -setcookie'. Maintenance commands (ctl/remote_console/stop/...) pick up
# the already-resolved cookie from the running node's 'ps' output, so they must
# NOT re-read the file (a FIFO would block forever waiting for a writer).
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
    case "$COOKIE" in "file://"*) COOKIE_FROM_FILE='yes' ;; esac
    COOKIE="$(resolve_cookie_source "$COOKIE")"
    if [ -z "$COOKIE" ]; then
        ## Resolve the configured cookie only when there is no env override, so a
        ## 'file://' source is read at most once per boot.
        case "$COOKIE_IN_USE" in "file://"*) COOKIE_FROM_FILE='yes' ;; esac
        COOKIE_IN_USE="$(resolve_cookie_source "$COOKIE_IN_USE")"
    fi
fi
if [ "$IS_BOOT_COMMAND" != 'yes' ] && [ -n "$COOKIE_IN_USE" ] && [ -n "$COOKIE" ] && [ "$COOKIE" != "$COOKIE_IN_USE" ]; then
    die "EMQX_NODE__COOKIE is different from the cookie used by $NAME"
fi
[ -z "$COOKIE" ] && COOKIE="$COOKIE_IN_USE"
[ -z "$COOKIE" ] && COOKIE="$EMQX_DEFAULT_ERLANG_COOKIE"
# When the cookie came from a 'file://' URL, re-export the resolved value. The
# 'start' command re-execs 'bin/emqx console' via run_erl; the child inherits
# this and skips file resolution, so a write-once FIFO is read exactly once per
# boot instead of blocking the child forever.
if [ "$COOKIE_FROM_FILE" = 'yes' ]; then
    export EMQX_NODE__COOKIE="$COOKIE"
fi
[[ "$DEBUG" -gt 0 ]] && set -x

# Frequently used sample cookie value for testing, not recommended for production use
EMQX_SAMPLE_ERLANG_COOKIE="emqxsecretcookie"

maybe_warn_default_cookie() {
    # Turn off debug so the cookie value isn't traced by 'set -x'.
    set +x
    if [[ "$IS_BOOT_COMMAND" = 'yes' && ("$COOKIE" = "$EMQX_DEFAULT_ERLANG_COOKIE" || "$COOKIE" = "$EMQX_SAMPLE_ERLANG_COOKIE") ]]; then
        logwarn "Default (insecure) Erlang cookie is in use."
        logwarn "Configure node.cookie in $EMQX_ETC_DIR/emqx.conf or override from environment variable EMQX_NODE__COOKIE"
        logwarn "NOTE: Use the same cookie for all nodes in the cluster."
        if [ "$EMQX_SECURITY_PROFILE" != "legacy" ]; then
            die "Cannot continue with default cookie and EMQX_SECURITY_PROFILE set to '$EMQX_SECURITY_PROFILE'."
        fi
    fi
    if [ "$DEBUG" -gt 0 ]; then set -x; fi
}

## check if OTP version has mnesia_hook feature; if not, fallback to
## using Mnesia DB backend.
if [[ "$IS_BOOT_COMMAND" == 'yes' && "$(get_boot_config 'node.db_backend')" == "rlog" ]]; then
    if ! (echo -e "$COMPATIBILITY_INFO" | $GREP -q 'MNESIA_OK'); then
      logwarn "DB Backend is RLOG, but an incompatible OTP version has been detected. Falling back to using Mnesia DB backend."
      export EMQX_NODE__DB_BACKEND=mnesia
      export EMQX_NODE__ROLE=core
    fi
fi

diagnose_boot_failure_and_die() {
    local ps_line
    local app_status
    # Turn off debug: the ps output contains the '-setcookie' argument.
    set +x
    ps_line="$(find_emqx_process)"
    if [ -z "$ps_line" ]; then
        echo "Find more information in the latest log file: ${EMQX_LOG_DIR}/erlang.log.*"
        exit 1
    fi
    if [ "$DEBUG" -gt 0 ]; then set -x; fi
    if ! relx_nodetool "ping" > /dev/null; then
        logerr "$NAME seems to be running, but not responding to pings."
        echo "Make sure '$HOST_NAME' is a resolvable and reachable hostname."
        pipe_shutdown
        exit 2
    fi
    app_status="$(relx_nodetool 'eval' 'emqx:is_running()')"
    if [ "$app_status" != 'true' ]; then
        logerr "$NAME node is started, but failed to complete the boot sequence in time."
        pipe_shutdown
        exit 3
    fi
}

## Only works when started in daemon mode
## Returns non-zero when there is no pipe to shut down from.
pipe_shutdown() {
    ## `run_erl' creates the pipe files in PIPE_DIR. The directory is left
    ## behind when the node exits, so check that it still holds a pipe.
    if [ ! -d "$PIPE_DIR" ] || [ -z "$(ls -A "$PIPE_DIR" 2>/dev/null)" ]; then
        return 1
    fi
    echo "Shutting down $NAME from to_erl pipe."
    ## can not evaluate init:stop() or erlang:halt() because the shell is restricted
    echo 'emqx_machine:brutal_shutdown().' | "$BINDIR/to_erl" "$PIPE_DIR"
}

## Call nodetool to stop EMQX
nodetool_shutdown() {
    # Wait for the node to completely stop...
    ## When the node cannot be reached, nodetool has already logged the reason.
    ## Return here so the caller can try the to_erl pipe instead. Calling
    ## nodetool again would repeat the same message, and there is no PID to
    ## report a graceful shutdown failure for.
    if ! PID="$(relx_get_pid)"; then
        return 1
    fi
    if ! relx_nodetool "stop"; then
        die "Graceful shutdown failed PID=[$PID]"
    fi
    WAIT_TIME="${EMQX_WAIT_FOR_STOP:-120}"
    if ! wait_for "$WAIT_TIME" 'is_down' "$PID"; then
        msg="dangling after ${WAIT_TIME} seconds"
        # also log to syslog
        logger -t "${REL_NAME}[${PID}]" "STOP: $msg"
        # log to user console
        set +x
        logerr "Stop failed, $msg"
        echo "ERROR: $PID is still around"
        ps -p "$PID"
        exit 1
    fi
    echo "ok"
    logger -t "${REL_NAME}[${PID}]" "STOP: OK"
}

## make sure the CWD of emqx is BASE_RUNNER_ROOT_DIR, so relative paths like "etc/"
## and "data/" still work.
cd "$BASE_RUNNER_ROOT_DIR"

case "${COMMAND}" in
    start)
        maybe_warn_ulimit
        maybe_warn_default_cookie

        # this flag passes down to console mode
        # so we know it's intended to be run in daemon mode
        export _EMQX_START_DAEMON_MODE=1

        case "$COMMAND" in
            start)
                shift
                START_OPTION="console"
                HEART_OPTION="start"
                ;;
        esac
        RUN_PARAM="$*"

        # Set arguments for the heart command
        set -- "$RUNNER_SCRIPT" "$HEART_OPTION"
        [ "$RUN_PARAM" ] && set -- "$@" "$RUN_PARAM"

        # Export the HEART_COMMAND
        HEART_COMMAND="$RUNNER_SCRIPT $COMMAND"
        export HEART_COMMAND

        ## See: http://erlang.org/doc/man/run_erl.html
        # Export the RUN_ERL_LOG_GENERATIONS
        export RUN_ERL_LOG_GENERATIONS=${RUN_ERL_LOG_GENERATIONS:-"5"}

        # Export the RUN_ERL_LOG_MAXSIZE
        export RUN_ERL_LOG_MAXSIZE=${RUN_ERL_LOG_MAXSIZE:-"10485760"}

        mkdir -p "$PIPE_DIR"

        "$BINDIR/run_erl" -daemon "$PIPE_DIR" "$EMQX_LOG_DIR" \
                          "$(relx_start_command)"

        WAIT_TIME=${EMQX_WAIT_FOR_START:-120}
        if wait_until_return_val "true" "$WAIT_TIME" 'relx_nodetool' \
                'eval' 'emqx:is_running()'; then
            echo "$EMQX_DESCRIPTION $REL_VSN is started successfully!"
            exit 0
        else
            logerr "${EMQX_DESCRIPTION} ${REL_VSN} using node name '${NAME}' failed ${WAIT_TIME} probes."
            diagnose_boot_failure_and_die
        fi
        ;;

    stop)
        if ! nodetool_shutdown; then
            ## The node did not stop and there is no pipe to fall back to.
            pipe_shutdown || exit 1
        fi
        ;;

    pid)
        ## Get the VM's pid
        if ! relx_get_pid; then
            exit 1
        fi
        ;;

    ping)
        assert_node_alive
        echo pong
        ;;

    escript)
        ## Run an escript under the node's environment
        if ! relx_escript "$@"; then
            exit 1
        fi
        ;;

    attach)
        exec "$BINDIR/to_erl" "$PIPE_DIR"
        ;;

    remote|remote_console)
        assert_node_alive

        case "${2:-}" in
            --iex)
                EMQX_CONSOLE_FLAVOR=iex
                shift
                ;;
            "")
                ;;
            *)
                echo "Usage: $REL_NAME $COMMAND [--iex]"
                exit 1
                ;;
        esac
        shift
        remsh
        ;;

    console|console_clean|foreground)
        # Log version before start, help boot failure troubleshooting
        echo "Starting $EMQX_DESCRIPTION $REL_VSN"
        if [ -n "${EMQX_FEATURES:-}" ]; then
            echo "EMQX_FEATURES=${EMQX_FEATURES}"
        fi
        # .boot file typically just $REL_NAME (ie, the app name)
        # however, for debugging, sometimes start_clean.boot is useful.
        # For e.g. 'setup', one may even want to name another boot script.
        case "$COMMAND" in
            console|foreground)
                if [ -f "$REL_DIR/$REL_NAME.boot" ]; then
                  BOOTFILE="$REL_DIR/$REL_NAME"
                else
                  BOOTFILE="$REL_DIR/start"
                fi
                ;;
            console_clean)
                BOOTFILE="$REL_DIR/start_clean"
                ;;
        esac
        case "$COMMAND" in
            foreground)
                FOREGROUNDOPTIONS="-noinput -noshell +Bd"
                ;;
            *)
                FOREGROUNDOPTIONS=''
                ;;
        esac

        # set before generate_config
        if [ "${_EMQX_START_DAEMON_MODE:-}" = 1 ]; then
            tr_log_to_env
        else
            maybe_log_to_console
            maybe_warn_ulimit
            maybe_warn_default_cookie
        fi

        #generate app.config and vm.args
        generate_config "$NAME_TYPE" "$NAME"

        check_license

        # Setup beam-required vars
        EMU="beam"
        PROGNAME="${0}"

        export EMU
        export PROGNAME

        case "${COMMAND}" in
            console|console_clean)
                case "${2:-}" in
                    --iex)
                        EMQX_CONSOLE_FLAVOR=iex
                        shift
                        ;;
                    "")
                        ;;
                    *)
                        echo "Usage: $REL_NAME $COMMAND [--iex]"
                        exit 1
                        ;;
                esac
                ;;
            *)
                ;;
        esac

        # Store passed arguments since they will be erased by `set`
        ARGS="$*"

        # Turn off debug (and keep it off through the exec below) so the
        # cookie in the command arguments isn't traced by 'set -x'.
        set +x

        # shellcheck disable=SC2086
        # Build an array of arguments to pass to exec later on
        # Build it here because this command will be used for logging.
        if [ "${EMQX_CONSOLE_FLAVOR:-erl}" = 'erl' ] ; then
            if [[ "$DEBUG" == 2 ]]; then
              INIT_DEBUG_ARG="-init_debug"
            else
              INIT_DEBUG_ARG=""
            fi

            # pass down RELEASE_LIB so we can switch to EMQX_CONSOLE_FLAVOR=erl
            # to boot an Erlang node from the elixir release
            set -- "$BINDIR/erlexec" \
                $FOREGROUNDOPTIONS \
                -boot "$BOOTFILE" \
                -boot_var RELEASE_LIB "$ERTS_LIB_DIR" \
                -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \
                -mode "$CODE_LOADING_MODE" \
                -config "$CONF_FILE" \
                -args_file "$ARGS_FILE" \
                -setcookie "$COOKIE" \
                $INIT_DEBUG_ARG \
                $EPMD_ARGS
        else
            if [[ "$DEBUG" == 2 ]]; then
              INIT_DEBUG_ARG="--erl -init_debug"
            else
              INIT_DEBUG_ARG=""
            fi

            set -- "$REL_DIR/iex" \
                --boot "$BOOTFILE" \
                --boot-var RELEASE_LIB "${ERTS_LIB_DIR}" \
                --erl-config "${CONF_FILE}" \
                --vm-args "${ARGS_FILE}" \
                --cookie "$COOKIE" \
                --erl "$FOREGROUNDOPTIONS" \
                --erl "-mode $CODE_LOADING_MODE" \
                --erl "$EPMD_ARGS" \
                $INIT_DEBUG_ARG
        fi

        # Log the startup, with the cookie masked
        logger -t "${REL_NAME}[$$]" "EXEC: $(redact_cookie_args "$@") -- ${1+$ARGS} -ekka_proto_dist ${EKKA_PROTO_DIST_MOD} -emqx_data_dir ${DATA_DIR}"

        # Start the VM
        # add ekka_proto_dist emqx_data_dir to boot command so it is visible from 'ps -ef'
        # NTOE: order matters! emqx_data_dir has to be positioned at the end of the line to simplify the
        # line parsing when file path contains spaces
        exec "$@" -- ${1+$ARGS} -ekka_proto_dist "${EKKA_PROTO_DIST_MOD}" -emqx_data_dir "${DATA_DIR}"
        ;;

    ctl)
        assert_node_alive

        shift

        relx_nodetool rpc_infinity emqx_ctl run_command "$@"
        ;;
    rpc)
        assert_node_alive

        shift

        relx_nodetool rpc "$@"
        ;;
    rpcterms)
        assert_node_alive

        shift

        relx_nodetool rpcterms "$@"
        ;;
    eval)
        assert_node_alive

        shift
        relx_nodetool "eval" "$@"
        ;;
    eval-ex)
        assert_node_alive

        shift
        # Turn off debug so the cookie value isn't traced by 'set -x'.
        set +x
        "$REL_DIR/elixir" \
            --hidden \
            --name "rand-$(gen_node_id)-$NAME" \
            --cookie "$COOKIE" \
            --boot "$REL_DIR/start_clean" \
            --boot-var RELEASE_LIB "$ERTS_LIB_DIR" \
            --erl "-start_epmd false" \
            --erl "-epmd_module ekka_epmd" \
            --erl "-kernel standard_io_encoding unicode" \
            --erl "+P 65536" \
            --erl "+Q 65536" \
            --erl "+S 2" \
            --rpc-eval "$NAME" "$@"
        ;;
    check_config)
        check_config
        ;;
    *)
        usage "$COMMAND"
        exit 1
        ;;
esac

exit 0
