541 lines
14 KiB
Bash
Executable File
541 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
if [ -n "${DEBUG:-}" ]; then
|
|
set -x
|
|
fi
|
|
|
|
UNAME="$(uname -s)"
|
|
|
|
PROJ_ROOT="$(git rev-parse --show-toplevel)"
|
|
cd "$PROJ_ROOT"
|
|
|
|
logerr() {
|
|
if [ "${TERM:-dumb}" = dumb ]; then
|
|
echo -e "ERROR: $*" 1>&2
|
|
else
|
|
echo -e "$(tput setaf 1)ERROR: $*$(tput sgr0)" 1>&2
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
|
|
Run EMQX without building a release (which takes longer time).
|
|
Node state is stored in '_build/dev-run/$PROFILE'.
|
|
The node is started in interactive mode without a boot file.
|
|
|
|
USAGE: $0 [COMMAND] [OPTION[]
|
|
|
|
COMMANDS:
|
|
help: Print this usage info.
|
|
run: Default command.
|
|
remsh: Attach to running node's remote console.
|
|
Target node name is to be specified with -n|--name,
|
|
otherwise defaults to 'emqx@127.0.0.1'.
|
|
ctl: Equivalent to 'emqx ctl'.
|
|
ctl command arguments should be passed after '--'
|
|
e.g. $0 ctl -- help
|
|
eval: Evaluate an Erlang expression
|
|
|
|
OPTIONS:
|
|
-h|--help: Print this usage info.
|
|
-p|--profile: emqx | emqx-enterprise, defaults to 'PROFILE' env.
|
|
-c|--compile: Force recompile, otherwise starts with the already built libs
|
|
in '_build/\$PROFILE/lib/'.
|
|
-e|--ekka-epmd: Force to use ekka_epmd.
|
|
-n|--name: Node name, defaults to \$EMQX_NODE__NAME or the \$EMQX_NODE_NAME env.
|
|
-s|--shortnames: Use shortnames for erlang node name (ie. use -sname parameter).
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
|
|
PROFILE: Overridden by '-p|--profile' option, defaults to 'emqx'.
|
|
EMQX_NODE_NAME: Overridden by '-n|--name' or '-r|--remsh' option.
|
|
The node name of the EMQX node. Default to emqx@127.0.0.1'.
|
|
EMQX_NODE_COOKIE: Erlang cookie, defaults to ~/.erlang.cookie
|
|
|
|
EOF
|
|
}
|
|
|
|
if [ -n "${DEBUG:-}" ]; then
|
|
set -x
|
|
fi
|
|
|
|
export HOCON_ENV_OVERRIDE_PREFIX='EMQX_'
|
|
case "${EMQX_DEFAULT_LOG_HANDLER:-console}" in
|
|
console|default)
|
|
export EMQX_LOG__FILE__DEFAULT__ENABLE='false'
|
|
export EMQX_LOG__CONSOLE__ENABLE='true'
|
|
;;
|
|
file)
|
|
export EMQX_LOG__FILE__DEFAULT__ENABLE='true'
|
|
export EMQX_LOG__CONSOLE__ENABLE='false'
|
|
;;
|
|
both)
|
|
export EMQX_LOG__CONSOLE__ENABLE='true'
|
|
export EMQX_LOG__FILE__ENABLE='true'
|
|
;;
|
|
*)
|
|
;;
|
|
esac
|
|
|
|
SYSTEM="$(./scripts/get-distro.sh)"
|
|
if [ -n "${EMQX_NODE_NAME:-}" ]; then
|
|
export EMQX_NODE__NAME="${EMQX_NODE_NAME}"
|
|
fi
|
|
EMQX_NODE_NAME="${EMQX_NODE__NAME:-emqx@127.0.0.1}"
|
|
PROFILE="${PROFILE:-emqx}"
|
|
FORCE_COMPILE=0
|
|
# Do not start using ekka epmd by default, so your IDE can connect to it
|
|
EKKA_EPMD=0
|
|
SHORTNAMES=0
|
|
COMMAND='run'
|
|
case "${1:-novalue}" in
|
|
novalue)
|
|
;;
|
|
run)
|
|
shift
|
|
;;
|
|
remsh)
|
|
COMMAND='remsh'
|
|
shift
|
|
;;
|
|
ctl)
|
|
COMMAND='ctl'
|
|
shift
|
|
;;
|
|
eval)
|
|
COMMAND='eval'
|
|
shift
|
|
;;
|
|
help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
-*)
|
|
;;
|
|
esac
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case $1 in
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
-n|--name)
|
|
EMQX_NODE_NAME="$2"
|
|
shift 1
|
|
;;
|
|
-p|--profile)
|
|
PROFILE="${2}"
|
|
shift 1;
|
|
;;
|
|
-c|--compile)
|
|
FORCE_COMPILE=1
|
|
;;
|
|
-e|--ekka-epmd)
|
|
EKKA_EPMD=1
|
|
;;
|
|
-s|--shortnames)
|
|
SHORTNAMES=1
|
|
;;
|
|
--)
|
|
shift
|
|
PASSTHROUGH_ARGS=("$@")
|
|
break
|
|
;;
|
|
*)
|
|
logerr "Unknown argument $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift 1;
|
|
done
|
|
|
|
case "${PROFILE}" in
|
|
ce|emqx)
|
|
PROFILE='emqx'
|
|
;;
|
|
ee|emqx-enterprise)
|
|
PROFILE='emqx-enterprise'
|
|
;;
|
|
ce-ex|emqx-elixir)
|
|
export IS_ELIXIR=yes
|
|
PROFILE='emqx'
|
|
;;
|
|
ee-ex|emqx-enterprise-elixir)
|
|
export IS_ELIXIR=yes
|
|
PROFILE='emqx-enterprise'
|
|
;;
|
|
*)
|
|
echo "Unknown profile $PROFILE"
|
|
exit 1
|
|
;;
|
|
esac
|
|
export PROFILE
|
|
|
|
case "${PROFILE}" in
|
|
emqx|emqx-elixir)
|
|
export SCHEMA_MOD='emqx_conf_schema'
|
|
;;
|
|
emqx-enterprise|emqx-enterprise-elixir)
|
|
export SCHEMA_MOD='emqx_enterprise_schema'
|
|
;;
|
|
esac
|
|
|
|
BASE_DIR="_build/dev-run/$PROFILE"
|
|
export EMQX_ETC_DIR="$BASE_DIR/etc"
|
|
export EMQX_DATA_DIR="$BASE_DIR/data"
|
|
export EMQX_LOG_DIR="$BASE_DIR/log"
|
|
export EMQX_PLUGINS__INSTALL_DIR="${EMQX_PLUGINS__INSTALL_DIR:-$BASE_DIR/plugins}"
|
|
CONFIGS_DIR="$EMQX_DATA_DIR/configs"
|
|
# Use your cookie so your IDE can connect to it.
|
|
COOKIE="${EMQX_NODE__COOKIE:-${EMQX_NODE_COOKIE:-$(cat ~/.erlang.cookie 2>/dev/null || echo 'emqxsecretcookie')}}"
|
|
mkdir -p "$EMQX_ETC_DIR" "$EMQX_DATA_DIR/patches" "$EMQX_DATA_DIR/plugins" "$EMQX_DATA_DIR/certs" "$EMQX_LOG_DIR" "$CONFIGS_DIR"
|
|
if [ $EKKA_EPMD -eq 1 ]; then
|
|
EPMD_ARGS='-start_epmd false -epmd_module ekka_epmd'
|
|
else
|
|
EPMD_ARGS=''
|
|
fi
|
|
|
|
if [ $SHORTNAMES -eq 1 ]; then
|
|
ERL_NAME_ARG='-sname'
|
|
else
|
|
ERL_NAME_ARG='-name'
|
|
fi
|
|
|
|
|
|
## build compile the profile is it's not compiled yet
|
|
prepare_erl_libs() {
|
|
local profile="$1"
|
|
local libs_dir="_build/${profile}/lib"
|
|
local erl_libs="${ERL_LIBS:-}"
|
|
local sep
|
|
if [ "${SYSTEM}" = 'windows' ]; then
|
|
sep=';'
|
|
else
|
|
sep=':'
|
|
fi
|
|
if [ $FORCE_COMPILE -eq 1 ] || [ ! -d "$libs_dir" ]; then
|
|
make "compile-${PROFILE}"
|
|
else
|
|
echo "Running from code in $libs_dir"
|
|
fi
|
|
for app in "${libs_dir}"/*; do
|
|
if [ -n "$erl_libs" ]; then
|
|
erl_libs="${erl_libs}${sep}${app}"
|
|
else
|
|
erl_libs="${app}"
|
|
fi
|
|
done
|
|
if [ "${IS_ELIXIR:-}" = "yes" ]; then
|
|
local elixir_libs_root_dir
|
|
elixir_libs_root_dir=$(realpath "$(elixir -e ':code.lib_dir(:elixir) |> IO.puts()')"/..)
|
|
for app in "${elixir_libs_root_dir}"/*; do
|
|
if [ -n "$erl_libs" ]; then
|
|
erl_libs="${erl_libs}${sep}${app}"
|
|
else
|
|
erl_libs="${app}"
|
|
fi
|
|
done
|
|
fi
|
|
if [ -e "_build/${profile}/checkouts" ]; then
|
|
for app in "_build/${profile}/checkouts"/*; do
|
|
erl_libs="${erl_libs}${sep}${app}"
|
|
done
|
|
fi
|
|
export ERL_LIBS="$erl_libs"
|
|
}
|
|
|
|
## poorman's mustache templating
|
|
mustache() {
|
|
local name="$1"
|
|
local value="$2"
|
|
local file="$3"
|
|
if [[ "$UNAME" == "Darwin" ]]; then
|
|
sed -i '' "s|{{[[:space:]]*${name}[[:space:]]*}}|${value}|g" "$file"
|
|
else
|
|
sed -i "s|{{\s*${name}\s*}}|${value}|g" "$file"
|
|
fi
|
|
}
|
|
|
|
## render the merged boot conf file.
|
|
## the merge action is done before the profile is compiled
|
|
render_hocon_conf() {
|
|
input="apps/emqx_conf/etc/emqx.conf.all"
|
|
output="$EMQX_ETC_DIR/emqx.conf"
|
|
cp "$input" "$output"
|
|
mustache emqx_default_erlang_cookie "$COOKIE" "$output"
|
|
mustache platform_data_dir "${EMQX_DATA_DIR}" "$output"
|
|
mustache platform_log_dir "${EMQX_LOG_DIR}" "$output"
|
|
mustache platform_etc_dir "${EMQX_ETC_DIR}" "$output"
|
|
}
|
|
|
|
## Make comma separated quoted strings
|
|
make_erlang_args() {
|
|
local in=("$@")
|
|
local args=''
|
|
for arg in "${in[@]}"; do
|
|
if [ -z "$args" ]; then
|
|
args="\"$arg\""
|
|
else
|
|
args="$args, \"$arg\""
|
|
fi
|
|
done
|
|
echo "$args"
|
|
}
|
|
|
|
call_hocon() {
|
|
local args erl_code
|
|
args="$(make_erlang_args "$@")"
|
|
erl_code="
|
|
{ok, _} = application:ensure_all_started(hocon), \
|
|
try
|
|
mnesia_hook:module_info()
|
|
catch _:_->
|
|
io:format(standard_error, \"Force setting DB backend to 'mnesia', and 'role' to 'core'~n\", []),
|
|
os:putenv(\"EMQX_NODE__DB_BACKEND\", \"mnesia\"),
|
|
os:putenv(\"EMQX_NODE__DB_ROLE\", \"core\")
|
|
end,
|
|
{Time, ok} = timer:tc(fun() -> ok = hocon_cli:main([$args]) end),
|
|
io:format(user, \"Took ~pms to generate config~n\", [Time div 1000]),
|
|
init:stop().
|
|
"
|
|
erl -noshell -eval "$erl_code"
|
|
}
|
|
|
|
# Function to generate app.config and vm.args
|
|
# sets two environment variables CONF_FILE and ARGS_FILE
|
|
generate_app_conf() {
|
|
## 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:
|
|
## - $DATA_DIR/cluster.hocon (if exists)
|
|
## - 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_DATA_DIR"/configs/cluster.hocon \
|
|
-c "$EMQX_ETC_DIR"/emqx.conf \
|
|
-d "$EMQX_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"
|
|
}
|
|
|
|
# apps/emqx/etc/vm.args.cloud
|
|
append_args_file() {
|
|
## ensure a new line at the end
|
|
echo '' >> "$ARGS_FILE"
|
|
cat <<EOF >> "$ARGS_FILE"
|
|
$ERL_NAME_ARG $EMQX_NODE_NAME
|
|
-mnesia dir '"$EMQX_DATA_DIR/mnesia/$EMQX_NODE_NAME"'
|
|
-stdlib restricted_shell emqx_restricted_shell
|
|
+spp true
|
|
+A 4
|
|
+IOt 4
|
|
+SDio 8
|
|
-shutdown_time 30000
|
|
-pa '$EMQX_DATA_DIR/patches'
|
|
-mnesia dump_log_write_threshold 5000
|
|
-mnesia dump_log_time_threshold 60000
|
|
-os_mon start_disksup false
|
|
EOF
|
|
}
|
|
|
|
# copy cert files and acl.conf to etc
|
|
copy_other_conf_files() {
|
|
cp -r apps/emqx/etc/certs "$EMQX_ETC_DIR"/
|
|
cp apps/emqx_auth/etc/acl.conf "$EMQX_ETC_DIR"/
|
|
}
|
|
|
|
is_current_profile_app() {
|
|
local app="$1"
|
|
case "$app" in
|
|
*emqx_telemetry*)
|
|
if [ "$PROFILE" = 'emqx-enterprise' ]; then
|
|
return 1
|
|
else
|
|
return 0
|
|
fi
|
|
;;
|
|
*)
|
|
if [ "$PROFILE" = 'emqx' ]; then
|
|
if [ -f "$app"/BSL.txt ]; then
|
|
return 1
|
|
else
|
|
return 0
|
|
fi
|
|
else
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
## apps to load
|
|
apps_to_load() {
|
|
local apps csl
|
|
apps="$(./scripts/find-apps.sh | xargs)"
|
|
csl=""
|
|
for app in $apps; do
|
|
if ! is_current_profile_app "$app"; then
|
|
continue
|
|
fi
|
|
name="$(basename "$app")"
|
|
if [ -z "$csl" ]; then
|
|
csl="$name"
|
|
else
|
|
csl="$csl,$name"
|
|
fi
|
|
done
|
|
echo "$csl"
|
|
}
|
|
|
|
boot() {
|
|
## Make erl command aware where to load all the beams
|
|
## this should be done before every erl command
|
|
prepare_erl_libs "$PROFILE"
|
|
## make sure copy acl.conf and certs to etc before render_hocon_conf
|
|
## hocon will check rules inside acl.conf.
|
|
copy_other_conf_files
|
|
render_hocon_conf
|
|
generate_app_conf
|
|
append_args_file
|
|
APPS="$(apps_to_load)"
|
|
|
|
if [ "${IS_ELIXIR:-}" = "yes" ]; then
|
|
BOOT_SEQUENCE='
|
|
apps = System.get_env("APPS") |> String.split(",") |> Enum.map(&String.to_atom/1)
|
|
Enum.each(apps, &Application.load/1)
|
|
IO.inspect(apps, label: :loaded, limit: :infinity)
|
|
{:ok, _} = Application.ensure_all_started(:emqx_machine)
|
|
'
|
|
if [ -n "${EPMD_ARGS:-}" ]; then
|
|
EPMD_ARGS_ELIXIR="$EPMD_ARGS"
|
|
else
|
|
EPMD_ARGS_ELIXIR="-no_op true"
|
|
fi
|
|
local OTP_VSN USER_MOD_ARG
|
|
OTP_VSN=$(./scripts/get-otp-vsn.sh)
|
|
case "$OTP_VSN" in
|
|
25*)
|
|
USER_MOD_ARG='-user Elixir.IEx.CLI'
|
|
;;
|
|
*)
|
|
USER_MOD_ARG='-user elixir'
|
|
;;
|
|
esac
|
|
|
|
# shellcheck disable=SC2086
|
|
env APPS="$APPS" iex \
|
|
-$ERL_NAME_ARG "$EMQX_NODE_NAME" \
|
|
--erl "$EPMD_ARGS_ELIXIR" \
|
|
--erl "$USER_MOD_ARG" \
|
|
--erl '-proto_dist ekka' \
|
|
--vm-args "$ARGS_FILE" \
|
|
--erl-config "$CONF_FILE" \
|
|
-e "$BOOT_SEQUENCE"
|
|
else
|
|
BOOT_SEQUENCE="
|
|
Apps=[${APPS}],
|
|
ok=lists:foreach(fun application:load/1, Apps),
|
|
io:format(user, \"~nLoaded: ~p~n\", [Apps]),
|
|
{ok, _} = application:ensure_all_started(emqx_machine).
|
|
"
|
|
|
|
# shellcheck disable=SC2086
|
|
erl $ERL_NAME_ARG "$EMQX_NODE_NAME" \
|
|
$EPMD_ARGS \
|
|
-proto_dist ekka \
|
|
-args_file "$ARGS_FILE" \
|
|
-config "$CONF_FILE" \
|
|
-s emqx_restricted_shell set_prompt_func \
|
|
-eval "$BOOT_SEQUENCE"
|
|
fi
|
|
}
|
|
|
|
# Generate a random id
|
|
gen_tmp_node_name() {
|
|
local rnd
|
|
rnd="$(od -t u -N 4 /dev/urandom | head -n1 | awk '{print $2 % 1000}')"
|
|
if [ $SHORTNAMES -eq 1 ]; then
|
|
echo "remsh${rnd}"
|
|
else
|
|
echo "remsh${rnd}-${EMQX_NODE_NAME}"
|
|
fi
|
|
}
|
|
|
|
remsh() {
|
|
local tmpnode
|
|
tmpnode="$(gen_tmp_node_name)"
|
|
# shellcheck disable=SC2086
|
|
erl $ERL_NAME_ARG "$tmpnode" \
|
|
-hidden \
|
|
-setcookie "$COOKIE" \
|
|
-remsh "$EMQX_NODE_NAME" \
|
|
$EPMD_ARGS
|
|
}
|
|
|
|
# evaluate erlang expression in remsh node
|
|
eval_remsh_erl() {
|
|
local tmpnode erl_code
|
|
tmpnode="$(gen_tmp_node_name)"
|
|
erl_code="$1"
|
|
# shellcheck disable=SC2086 # need to expand EMQD_ARGS
|
|
erl $ERL_NAME_ARG "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$erl_code" 2>&1
|
|
}
|
|
|
|
ctl() {
|
|
if [ -z "${PASSTHROUGH_ARGS:-}" ]; then
|
|
logerr "Need at least one argument for ctl command"
|
|
logerr "e.g. $0 ctl -- help"
|
|
exit 1
|
|
fi
|
|
local args rpc_code output result
|
|
args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")"
|
|
rpc_code="
|
|
case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of
|
|
ok ->
|
|
init:stop(0);
|
|
true ->
|
|
init:stop(0);
|
|
Error ->
|
|
io:format(\"~p~n\", [Error]),
|
|
init:stop(1)
|
|
end"
|
|
set +e
|
|
output="$(eval_remsh_erl "$rpc_code")"
|
|
result=$?
|
|
if [ $result -eq 0 ]; then
|
|
echo -e "$output"
|
|
else
|
|
logerr "$output"
|
|
fi
|
|
exit $result
|
|
}
|
|
|
|
case "$COMMAND" in
|
|
run)
|
|
boot
|
|
;;
|
|
remsh)
|
|
remsh
|
|
;;
|
|
ctl)
|
|
ctl
|
|
;;
|
|
eval)
|
|
PASSTHROUGH_ARGS=('eval_erl' "${PASSTHROUGH_ARGS[@]}")
|
|
ctl
|
|
;;
|
|
esac
|