emqx/bin/emqx
Zaiming (Stone) Shi 9bc732784e fix(bin/emqx): check app status in bash script
Prior to this change, 'true=emqx:is_running().' Erlang assertion
is evaluated before 'emqx start' command gives up waiting.

The Erlang assertion would result in a error level crash log
which can be confusing.

This commit changes the equals to 'true' check to bash script
instead.

Also deleted the log message which advised users to report an
issue in case timeout happens, because daemon mode is almost only
used in tests
2024-01-08 17:15:34 +01:00

1313 lines
43 KiB
Bash
Executable File

#!/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 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"
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/<relname>-<version>.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/<relname>-<version>.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/<relname>-<version>.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/<relname>-<version>.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.<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)"
# data_dir is actually not needed, but kept anyway
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
logdebug "EMQX_BOOT_CONFIGS: $EMQX_BOOT_CONFIGS"
[ "$DEBUG" -eq 1 ] && set -x
get_boot_config() {
path_to_value="$1"
echo -e "$EMQX_BOOT_CONFIGS" | $GREP "$path_to_value=" | sed -e "s/$path_to_value=//g" | tr -d \"
}
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 '120')"
# 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="${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
key_license="${EMQX_LICENSE__KEY:-$(get_boot_config 'license.key')}"
if [[ -n "$key_license" && ("$key_license" != "undefined") ]]; then
call_nodetool check_license_key "$key_license"
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
}
# When deciding which install upgrade script to run, we have to check
# our own version so we may avoid infinite loops and call the correct
# version.
current_script_version() {
curr_script=$(basename "${BASH_SOURCE[0]}")
suffix=${curr_script#*-}
if [[ "${suffix}" == "${curr_script}" ]]; then
# there's no suffix, so we're running the default `emqx` script;
# we'll have to trust the REL_VSN variable
echo "$REL_VSN"
else
echo "${suffix}"
fi
}
parse_semver() {
echo "$1" | tr '.|-' ' '
}
max_version_of() {
local vsn1="$1"
local vsn2="$2"
echo "${vsn1}" "${vsn2}" | tr " " "\n" | sort -rV | head -n1
}
versioned_script_path() {
local script_name="$1"
local vsn="$2"
echo "$RUNNER_ROOT_DIR/bin/$script_name-$vsn"
}
does_script_version_exist() {
local script_name="$1"
local vsn="$2"
if [[ -f "$(versioned_script_path "$script_name" "$vsn")" ]]; then
return 0
else
return 1
fi
}
# extract_from_package packege_path destination file1 file2
extract_from_package() {
local package="$1"
local dest_dir="$2"
shift 2
tar -C "$dest_dir" -xf "$package" "$@"
}
am_i_the_newest_script() {
local curr_vsn other_vsn
curr_vsn="$(current_script_version)"
other_vsn="$1"
max_vsn="$(max_version_of "$other_vsn" "$curr_vsn")"
if [[ "$max_vsn" == "$curr_vsn" ]]; then
return 0
else
return 1
fi
}
locate_package() {
local package_path candidates vsn
vsn="$1"
if [[ "${IS_ENTERPRISE}" == "yes" ]]; then
package_pattern="$RUNNER_ROOT_DIR/releases/emqx-enterprise-$vsn-*.tar.gz"
else
package_pattern="$RUNNER_ROOT_DIR/releases/emqx-$vsn-*.tar.gz"
fi
# shellcheck disable=SC2207,SC2086
candidates=($(ls $package_pattern))
if [[ "${#candidates[@]}" == 0 ]]; then
logerr "No package matching $package_pattern found."
exit 1
elif [[ "${#candidates[@]}" -gt 1 ]]; then
logerr "Multiple packages matching $package_pattern found. Ensure only one exists."
exit 1
else
echo "${candidates[0]}"
fi
}
ensure_newest_script_is_extracted() {
local newest_vsn="$1"
local package_path tmpdir
if does_script_version_exist "emqx" "$newest_vsn" \
&& does_script_version_exist "install_upgrade.escript" "$newest_vsn"; then
return
else
package_path="$(locate_package "$newest_vsn")"
tmpdir="$(mktemp -dp /tmp emqx.XXXXXXXXXXX)"
extract_from_package \
"$package_path" \
"$tmpdir" \
"bin/emqx-$newest_vsn" \
"bin/install_upgrade.escript-$newest_vsn"
cp "$tmpdir/bin/emqx-$newest_vsn" \
"$tmpdir/bin/install_upgrade.escript-$newest_vsn" \
"$RUNNER_ROOT_DIR/bin/"
rm -rf "$tmpdir"
fi
}
# Run an escript in the node's environment
relx_escript() {
shift; scriptpath="$1"; shift
"$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"/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
## NOTE: the generate command merges environment variables to the base config (emqx.conf),
## but does not include the cluster-override.conf and local-override.conf
## meaning, certain overrides will not be mapped to app.<time>.config file
call_hocon -v -t "$NOW_TIME" -s "$SCHEMA_MOD" -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
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
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
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 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
COOKIE="${EMQX_NODE__COOKIE:-}"
COOKIE_IN_USE="$(get_boot_config 'node.cookie')"
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"
maybe_warn_default_cookie() {
if [ $IS_BOOT_COMMAND = 'yes' ] && [ "$COOKIE" = "$EMQX_DEFAULT_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."
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__DB_ROLE=core
fi
fi
diagnose_boot_failure_and_die() {
local ps_line
local app_status
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 ! 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
pipe_shutdown() {
if [ -d "$PIPE_DIR" ]; then
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"
fi
}
## Call nodetool to stop EMQX
nodetool_shutdown() {
# Wait for the node to completely stop...
PID="$(relx_get_pid)"
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"
}
cd "$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
pipe_shutdown
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_console)
assert_node_alive
shift
remsh
;;
upgrade|downgrade|install|unpack|uninstall)
if [ -z "${2:-}" ]; then
echo "Missing version argument"
echo "Usage: $REL_NAME $COMMAND {version}"
exit 1
fi
shift
assert_node_alive
curr_vsn="$(current_script_version)"
target_vsn="$1"
newest_vsn="$(max_version_of "$target_vsn" "$curr_vsn")"
ensure_newest_script_is_extracted "$newest_vsn"
# if we are not the newest script, run the same command from it
if ! am_i_the_newest_script "$newest_vsn"; then
script_path="$(versioned_script_path emqx "$newest_vsn")"
exec "$script_path" "$COMMAND" "$@"
fi
upgrade_script_path="$(versioned_script_path install_upgrade.escript "$newest_vsn")"
echo "using ${upgrade_script_path} to run ${COMMAND} $*"
ERL_FLAGS="${ERL_FLAGS:-} $EPMD_ARGS" \
exec "$BINDIR/escript" "$upgrade_script_path" \
"$COMMAND" "{'$REL_NAME', \"$NAME_TYPE\", '$NAME', '$COOKIE'}" "$@"
;;
versions)
assert_node_alive
shift
ERL_FLAGS="${ERL_FLAGS:-} $EPMD_ARGS" \
exec "$BINDIR/escript" "$RUNNER_ROOT_DIR/bin/install_upgrade.escript" \
"versions" "{'$REL_NAME', \"$NAME_TYPE\", '$NAME', '$COOKIE'}" "$@"
;;
console|console_clean|foreground)
# .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="-noshell -noinput +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
# Store passed arguments since they will be erased by `set`
ARGS="$*"
# 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 [ "$IS_ELIXIR" = no ] || [ "${EMQX_CONSOLE_FLAVOR:-}" = 'erl' ] ; then
# pass down RELEASE_LIB so we can switch to IS_ELIXIR=no
# 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" \
$EPMD_ARGS
else
set -- "$REL_DIR/iex" \
--boot "$BOOTFILE" \
--boot-var RELEASE_LIB "${ERTS_LIB_DIR}" \
--erl-config "${CONF_FILE}" \
--vm-args "${ARGS_FILE}" \
--erl "$FOREGROUNDOPTIONS" \
--erl "-mode $CODE_LOADING_MODE" \
--erl "$EPMD_ARGS" \
--werl
fi
# Log the startup
logger -t "${REL_NAME}[$$]" "EXEC: $* -- ${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
if [ "$IS_ELIXIR" = "yes" ]
then
"$REL_DIR/elixir" \
--hidden \
--name "rand-$(gen_node_id)-$NAME" \
--cookie "$COOKIE" \
--boot "$REL_DIR/start_clean" \
--boot-var RELEASE_LIB "$ERTS_LIB_DIR" \
--vm-args "$REL_DIR/remote.vm.args" \
--erl "-start_epmd false -epmd_module ekka_epmd" \
--rpc-eval "$NAME" "$@"
else
echo "EMQX node is not an Elixir node"
usage "$COMMAND"
exit 1
fi
;;
check_config)
check_config
;;
*)
usage "$COMMAND"
exit 1
;;
esac
exit 0