From ad6193884f09d29c7402de45214817dc195f5c6d Mon Sep 17 00:00:00 2001 From: Luis Rascao Date: Sat, 5 Nov 2016 22:11:44 +0000 Subject: Refactor foreground command Move it next to console since it's mostly the same thing but with extra parameters. --- priv/templates/extended_bin | 48 +++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/priv/templates/extended_bin b/priv/templates/extended_bin index acf77b9..bd7342d 100755 --- a/priv/templates/extended_bin +++ b/priv/templates/extended_bin @@ -435,8 +435,9 @@ case "$1" in "versions" "{'$REL_NAME', \"$NAME_TYPE\", '$NAME', '$COOKIE'}" "$@" ;; - console|console_clean|console_boot) + console|console_clean|console_boot|foreground) __code_paths="" + FOREGROUNDOPTIONS="" # .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. @@ -448,6 +449,16 @@ case "$1" in BOOTFILE="$REL_DIR/start" fi ;; + foreground) + # start up the release in the foreground for use by runit + # or other supervision services + if [ -f "$REL_DIR/$REL_NAME.boot" ]; then + BOOTFILE="$REL_DIR/$REL_NAME" + else + BOOTFILE="$REL_DIR/start" + fi + FOREGROUNDOPTIONS="-noshell -noinput +Bd" + ;; console_clean) __code_paths=$(relx_get_code_paths) BOOTFILE="$ROOTDIR/bin/start_clean" @@ -470,7 +481,8 @@ case "$1" in # Build an array of arguments to pass to exec later on # Build it here because this command will be used for logging. - set -- "$BINDIR/erlexec" -boot "$BOOTFILE" -mode "$CODE_LOADING_MODE" \ + set -- "$BINDIR/erlexec" $FOREGROUNDOPTIONS \ + -boot "$BOOTFILE" -mode "$CODE_LOADING_MODE" \ -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ -config "$RELX_CONFIG_PATH" \ -args_file "$VMARGS_PATH" \ @@ -484,38 +496,6 @@ case "$1" in echo "$RELEASE_ROOT_DIR" logger -t "$REL_NAME[$$]" "Starting up" - # Start the VM - exec "$@" -- ${1+$ARGS} - ;; - - foreground) - # start up the release in the foreground for use by runit - # or other supervision services - - [ -f "$REL_DIR/$REL_NAME.boot" ] && BOOTFILE="$REL_NAME" || BOOTFILE=start - FOREGROUNDOPTIONS="-noshell -noinput +Bd" - - # Setup beam-required vars - EMU=beam - PROGNAME="${0#*/}" - - export EMU - export PROGNAME - - # Store passed arguments since they will be erased by `set` - ARGS="$@" - - # Build an array of arguments to pass to exec later on - # Build it here because this command will be used for logging. - set -- "$BINDIR/erlexec" $FOREGROUNDOPTIONS \ - -boot "$REL_DIR/$BOOTFILE" -mode "$CODE_LOADING_MODE" -config "$RELX_CONFIG_PATH" \ - -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ - -args_file "$VMARGS_PATH" - - # Dump environment info for logging purposes - echo "Exec: $@" -- ${1+$ARGS} - echo "Root: $ROOTDIR" - # Start the VM exec "$@" -- ${1+$ARGS} ;; -- cgit v1.2.3 From 3905d39d180efe125b9ef5351d1c3b27a3f825b0 Mon Sep 17 00:00:00 2001 From: Luis Rascao Date: Sat, 5 Nov 2016 22:22:19 +0000 Subject: Add support for new relx directive that provides start/stop shell script hooks New 'extended_start_script_hooks' directive that allows the developer to define six different hook shell scripts to be invoked at pre/post start/stop/install upgrade phases. Besides these custom defined scripts, other types of builtin scripts are also available, these offer pre-packaged functionality that can be used directly, they are: pid - writes the beam pid to a configurable file location (/var/run/.pid by default). wait_for_vm_start - waits for the vm to start (ie. when it responds to pings) wait_for_process - waits for a configurable name to appear in the erlang process registry The hook scripts are invoked with the 'source' command, therefore they have access to all the variables in the start script. --- priv/templates/builtin_hook_pid | 12 ++ priv/templates/builtin_hook_wait_for_process | 17 ++ priv/templates/builtin_hook_wait_for_vm_start | 7 + priv/templates/extended_bin | 33 ++++ src/rlx_prv_assembler.erl | 106 ++++++++++- test/rlx_extended_bin_SUITE.erl | 248 +++++++++++++++++++++++++- test/rlx_test_utils.erl | 100 ++++++++++- 7 files changed, 515 insertions(+), 8 deletions(-) create mode 100644 priv/templates/builtin_hook_pid create mode 100644 priv/templates/builtin_hook_wait_for_process create mode 100644 priv/templates/builtin_hook_wait_for_vm_start diff --git a/priv/templates/builtin_hook_pid b/priv/templates/builtin_hook_pid new file mode 100644 index 0000000..0151631 --- /dev/null +++ b/priv/templates/builtin_hook_pid @@ -0,0 +1,12 @@ +#!/bin/bash + +# loop until the VM starts responding to pings +while ! $(relx_nodetool "ping">/dev/null) +do + sleep 1 +done + +# get the beam pid and write it to the file passed as +# argument +PID="$(relx_get_pid)" +echo $PID > $1 diff --git a/priv/templates/builtin_hook_wait_for_process b/priv/templates/builtin_hook_wait_for_process new file mode 100644 index 0000000..af5994d --- /dev/null +++ b/priv/templates/builtin_hook_wait_for_process @@ -0,0 +1,17 @@ +#!/bin/bash + +# loop until the VM starts responding to pings +while ! $(relx_nodetool "ping">/dev/null) +do + sleep 1 +done + +# loop until the name provided as argument gets +# registered +while true +do + if [ "$(relx_nodetool eval "whereis($1).")" != "undefined" ] + then + break + fi +done diff --git a/priv/templates/builtin_hook_wait_for_vm_start b/priv/templates/builtin_hook_wait_for_vm_start new file mode 100644 index 0000000..6b9ee12 --- /dev/null +++ b/priv/templates/builtin_hook_wait_for_vm_start @@ -0,0 +1,7 @@ +#!/bin/bash + +# loop until the VM starts responding to pings +while ! $(relx_nodetool "ping">/dev/null) +do + sleep 1 +done diff --git a/priv/templates/extended_bin b/priv/templates/extended_bin index bd7342d..c87fcf9 100755 --- a/priv/templates/extended_bin +++ b/priv/templates/extended_bin @@ -30,6 +30,14 @@ REL_DIR="$RELEASE_ROOT_DIR/releases/$REL_VSN" ERL_OPTS="{{ erl_opts }}" RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-$RELEASE_ROOT_DIR/log}" +# start/stop/install/upgrade pre/post hooks +PRE_START_HOOKS="{{ pre_start_hooks }}" +POST_START_HOOKS="{{ post_start_hooks }}" +PRE_STOP_HOOKS="{{ pre_stop_hooks }}" +POST_STOP_HOOKS="{{ post_stop_hooks }}" +PRE_INSTALL_UPGRADE_HOOKS="{{ pre_install_upgrade_hooks }}" +POST_INSTALL_UPGRADE_HOOKS="{{ post_install_upgrade_hooks }}" + relx_usage() { command="$1" @@ -235,6 +243,23 @@ check_replace_os_vars() { echo $OUT_FILE_PATH } +relx_run_hooks() { + HOOKS=$1 + for hook in $HOOKS + do + # the scripts arguments at this point are separated + # from each other by | , we now replace these + # by empty spaces and give them to the `set` + # command in order to be able to extract them + # separately + set `echo "$hook" | sed -e 's/|/ /g'` + HOOK_SCRIPT=$1; shift + # all hook locations are expected to be + # relative to the start script location + [ "$SCRIPT_DIR/$HOOK_SCRIPT" ] && . "$SCRIPT_DIR/$HOOK_SCRIPT" $@ + done +} + VMARGS_PATH=$(add_path vm.args $VMARGS_PATH) # Extract the target node name from node.args NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH" || true) @@ -330,11 +355,14 @@ case "$1" in mkdir -p "$PIPE_DIR" + relx_run_hooks "$PRE_START_HOOKS" "$BINDIR/run_erl" -daemon "$PIPE_DIR" "$RUNNER_LOG_DIR" \ "$(relx_start_command)" + relx_run_hooks "$POST_START_HOOKS" ;; stop) + relx_run_hooks "$PRE_STOP_HOOKS" # Wait for the node to completely stop... PID="$(relx_get_pid)" if ! relx_nodetool "stop"; then @@ -344,6 +372,7 @@ case "$1" in do sleep 1 done + relx_run_hooks "$POST_STOP_HOOKS" ;; restart) @@ -418,8 +447,12 @@ case "$1" in exit 1 fi + relx_run_hooks "$PRE_INSTALL_UPGRADE_HOOKS" + exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ "$COMMAND" "{'$REL_NAME', \"$NAME_TYPE\", '$NAME', '$COOKIE'}" "$@" + + relx_run_hooks "$POST_INSTALL_UPGRADE_HOOKS" ;; versions) diff --git a/src/rlx_prv_assembler.erl b/src/rlx_prv_assembler.erl index 7ff1d61..6074a1b 100644 --- a/src/rlx_prv_assembler.erl +++ b/src/rlx_prv_assembler.erl @@ -352,7 +352,12 @@ write_bin_file(State, Release, OutputDir, RelDir) -> %% extended start script needs nodetool so it's %% always included include_nodetool(BinDir), - extended_bin_file_contents(OsFamily, RelName, RelVsn, rlx_release:erts(Release), ErlOpts) + Hooks = expand_hooks(BinDir, + rlx_state:get(State, + extended_start_script_hooks, [])), + extended_bin_file_contents(OsFamily, RelName, RelVsn, + rlx_release:erts(Release), ErlOpts, + Hooks) end, %% We generate the start script by default, unless the user %% tells us not too @@ -383,6 +388,86 @@ write_bin_file(State, Release, OutputDir, RelDir) -> E end. +expand_hooks(_Bindir, []) -> []; +expand_hooks(BinDir, Hooks) -> + expand_hooks(BinDir, Hooks, []). + +expand_hooks(_BinDir, [], Acc) -> Acc; +expand_hooks(BinDir, [{Phase, Hooks0} | Rest], Acc) -> + %% filter and expand hooks to their respective shell scripts + Hooks = + lists:foldl( + fun(Hook, Acc0) -> + case validate_hook(Phase, Hook) of + true -> + %% all hooks are relative to the bin dir + HookScriptFilename = filename:join([BinDir, + hook_filename(Hook)]), + %% write the hook script file to it's proper location + ok = render_hook(hook_template(Hook), HookScriptFilename), + %% and return the invocation that's to be templated in the + %% extended script + Acc0 ++ [hook_invocation(Hook)]; + false -> + rebar_api:info("~p hook is not allowed in the ~p phase, ignoring it", + [Hook, Phase]), + Acc0 + end + end, [], Hooks0), + expand_hooks(BinDir, Rest, Acc ++ [{Phase, Hooks}]). + +%% the pid script hook is only allowed in the +%% post_start phase +%% with args +validate_hook(post_start, {pid, _}) -> true; +%% and without args +validate_hook(post_start, pid) -> true; +%% same for wait_for_vm_start, wait_for_process script +validate_hook(post_start, wait_for_vm_start) -> true; +validate_hook(post_start, {wait_for_process, _}) -> true; +%% custom hooks are allowed in all phases +validate_hook(_Phase, {custom, _}) -> true; +%% deny all others +validate_hook(_, _) -> false. + +hook_filename({custom, CustomScript}) -> CustomScript; +hook_filename(pid) -> "hooks/builtin/pid"; +hook_filename({pid, _}) -> "hooks/builtin/pid"; +hook_filename(wait_for_vm_start) -> "hooks/builtin/wait_for_vm_start"; +hook_filename({wait_for_process, _}) -> "hooks/builtin/wait_for_process". + +hook_invocation({custom, CustomScript}) -> CustomScript; +%% the pid builtin hook with no arguments writes to pid file +%% at /var/run/{{ rel_name }}.pid +hook_invocation(pid) -> string:join(["hooks/builtin/pid", + "/var/run/$REL_NAME.pid"], "|"); +hook_invocation({pid, PidFile}) -> string:join(["hooks/builtin/pid", + PidFile], "|"); +hook_invocation(wait_for_vm_start) -> "hooks/builtin/wait_for_vm_start"; +hook_invocation({wait_for_process, Name}) -> + %% wait_for_process takes an atom as argument + %% which is the process name to wait for + string:join(["hooks/builtin/wait_for_process", + atom_to_list(Name)], "|"). + +hook_template({custom, _}) -> custom; +hook_template(pid) -> builtin_hook_pid; +hook_template({pid, _}) -> builtin_hook_pid; +hook_template(wait_for_vm_start) -> builtin_hook_wait_for_vm_start; +hook_template({wait_for_process, _}) -> builtin_hook_wait_for_process. + +%% custom hooks are not rendered, they should +%% be copied by the release overlays +render_hook(custom, _) -> ok; +render_hook(TemplateName, Script) -> + rebar_api:debug("rendering ~p hook to ~p", + [TemplateName, Script]), + Template = render(TemplateName), + ok = filelib:ensure_dir(Script), + _ = ec_file:remove(Script), + ok = file:write_file(Script, Template), + ok = file:change_mode(Script, 8#755). + include_nodetool(BinDir) -> NodeToolFile = nodetool_contents(), InstallUpgradeFile = install_upgrade_escript_contents(), @@ -644,13 +729,28 @@ bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts) -> render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn}, {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}]). -extended_bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts) -> +extended_bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts, Hooks) -> Template = case OsFamily of unix -> extended_bin; win32 -> extended_bin_windows end, + %% turn all the hook lists into space separated strings + PreStartHooks = string:join(proplists:get_value(pre_start, Hooks, []), " "), + PostStartHooks = string:join(proplists:get_value(post_start, Hooks, []), " "), + PreStopHooks = string:join(proplists:get_value(pre_stop, Hooks, []), " "), + PostStopHooks = string:join(proplists:get_value(post_stop, Hooks, []), " "), + PreInstallUpgradeHooks = string:join(proplists:get_value(pre_install_upgrade, + Hooks, []), " "), + PostInstallUpgradeHooks = string:join(proplists:get_value(post_install_upgrade, + Hooks, []), " "), render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn}, - {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}]). + {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}, + {pre_start_hooks, PreStartHooks}, + {post_start_hooks, PostStartHooks}, + {pre_stop_hooks, PreStopHooks}, + {post_stop_hooks, PostStopHooks}, + {pre_install_upgrade_hooks, PreInstallUpgradeHooks}, + {post_install_upgrade_hooks, PostInstallUpgradeHooks}]). erl_ini(OutputDir, ErtsVsn) -> ErtsDirName = string:concat("erts-", ErtsVsn), diff --git a/test/rlx_extended_bin_SUITE.erl b/test/rlx_extended_bin_SUITE.erl index 2bd0554..99a45fb 100644 --- a/test/rlx_extended_bin_SUITE.erl +++ b/test/rlx_extended_bin_SUITE.erl @@ -32,7 +32,12 @@ replace_os_vars/1, replace_os_vars_custom_location/1, replace_os_vars_dev_mode/1, - replace_os_vars_twice/1]). + replace_os_vars_twice/1, + custom_start_script_hooks/1, + builtin_wait_for_vm_start_script_hook/1, + builtin_pid_start_script_hook/1, + builtin_wait_for_process_start_script_hook/1, + mixed_custom_and_builtin_start_script_hooks/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -58,9 +63,11 @@ init_per_testcase(_, Config) -> all() -> [ping, attach, pid, restart, reboot, escript, - remote_console, - replace_os_vars, replace_os_vars_custom_location, - replace_os_vars_dev_mode, replace_os_vars_twice]. + remote_console, replace_os_vars, + replace_os_vars_dev_mode, replace_os_vars_twice, + custom_start_script_hooks, builtin_wait_for_vm_start_script_hook, + builtin_pid_start_script_hook, builtin_wait_for_process_start_script_hook, + mixed_custom_and_builtin_start_script_hooks]. ping(Config) -> LibDir1 = proplists:get_value(lib1, Config), @@ -755,6 +762,239 @@ replace_os_vars_dev_mode(Config) -> {"COOKIE", "cookie2"}]), ok. +custom_start_script_hooks(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + + rlx_test_utils:create_app(LibDir1, "goal_app", "0.0.1", [stdlib,kernel], []), + + ConfigFile = filename:join([LibDir1, "relx.config"]), + rlx_test_utils:write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app]}, + {lib_dirs, [filename:join(LibDir1, "*")]}, + {generate_start_script, true}, + {extended_start_script, true}, + {extended_start_script_hooks, [ + {pre_start, [ + {custom, "hooks/pre_start"} + ]}, + {post_start, [ + {custom, "hooks/post_start"} + ]}, + {pre_stop, [ + {custom, "hooks/pre_stop"} + ]}, + {post_stop, [ + {custom, "hooks/post_stop"} + ]} + ]}, + {mkdir, "scripts"}, + {overlay, [{copy, "./pre_start", "bin/hooks/pre_start"}, + {copy, "./post_start", "bin/hooks/post_start"}, + {copy, "./pre_stop", "bin/hooks/pre_stop"}, + {copy, "./post_stop", "bin/hooks/post_stop"}]} + ]), + + %% write the hook scripts, each of them will write an erlang term to a file + %% that will later be consulted + ok = file:write_file(filename:join([LibDir1, "./pre_start"]), + "#!/bin/bash\n# $*\necho \\{pre_start, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + ok = file:write_file(filename:join([LibDir1, "./post_start"]), + "#!/bin/bash\n# $*\necho \\{post_start, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + ok = file:write_file(filename:join([LibDir1, "./pre_stop"]), + "#!/bin/bash\n# $*\necho \\{pre_stop, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + ok = file:write_file(filename:join([LibDir1, "./post_stop"]), + "#!/bin/bash\n# $*\necho \\{post_stop, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + {ok, _State} = relx:do(foo, undefined, [], [LibDir1], 3, + OutputDir, ConfigFile), + %% now start/stop the release to make sure the script hooks are really getting + %% executed + os:cmd(filename:join([OutputDir, "foo", "bin", "foo start"])), + timer:sleep(2000), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo stop"])), + %% now check that the output file contains the expected format + {ok,[{pre_start, foo, _, foo}, + {post_start, foo, _, foo}, + {pre_stop, foo, _, foo}, + {post_stop, foo, _, foo}]} = file:consult(filename:join([OutputDir, "foo", "test"])). + +builtin_pid_start_script_hook(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + + rlx_test_utils:create_app(LibDir1, "goal_app", "0.0.1", [stdlib,kernel], []), + + ConfigFile = filename:join([LibDir1, "relx.config"]), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + + rlx_test_utils:write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app]}, + {lib_dirs, [filename:join(LibDir1, "*")]}, + {generate_start_script, true}, + {extended_start_script, true}, + {extended_start_script_hooks, [ + {post_start, [ + {pid, filename:join([OutputDir, "foo.pid"])} + ]} + ]} + ]), + + {ok, _State} = relx:do(foo, undefined, [], [LibDir1], 3, + OutputDir, ConfigFile), + %% now start/stop the release to make sure the script hooks are really getting + %% executed + os:cmd(filename:join([OutputDir, "foo", "bin", "foo start"])), + %% check that the pid file really was created + ?assert(ec_file:exists(filename:join([OutputDir, "foo.pid"]))), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo stop"])), + ok. + +builtin_wait_for_vm_start_script_hook(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + + rlx_test_utils:create_app(LibDir1, "goal_app", "0.0.1", [stdlib,kernel], []), + + ConfigFile = filename:join([LibDir1, "relx.config"]), + rlx_test_utils:write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app]}, + {lib_dirs, [filename:join(LibDir1, "*")]}, + {generate_start_script, true}, + {extended_start_script, true}, + {extended_start_script_hooks, [ + {post_start, [wait_for_vm_start]} + ]} + ]), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + {ok, _State} = relx:do(foo, undefined, [], [LibDir1], 3, + OutputDir, ConfigFile), + %% now start/stop the release to make sure the script hooks are really getting + %% executed + os:cmd(filename:join([OutputDir, "foo", "bin", "foo start"])), + % this run doesn't need the sleep because the wait_for_vm_start + % start script makes it unnecessary + %timer:sleep(2000), + {ok, "pong"} = sh(filename:join([OutputDir, "foo", "bin", "foo ping"])), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo stop"])), + ok. + +builtin_wait_for_process_start_script_hook(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + + rlx_test_utils:create_full_app(LibDir1, "goal_app", "0.0.1", + [stdlib,kernel], []), + + ConfigFile = filename:join([LibDir1, "relx.config"]), + rlx_test_utils:write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app]}, + {lib_dirs, [filename:join(LibDir1, "*")]}, + {generate_start_script, true}, + {extended_start_script, true}, + {extended_start_script_hooks, [ + {post_start, [wait_for_vm_start, + {wait_for_process, goal_app_srv_signal}]} + ]} + ]), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + {ok, _State} = relx:do(foo, undefined, [], [LibDir1], 3, + OutputDir, ConfigFile), + %% now start/stop the release to make sure the script hooks are really getting + %% executed + %% get the current time, we'll measure how long it took for the node to + %% start, it must be at least 3 seconds which is the time it takes the + %% goal_app_srv to register the signal + T1 = os:timestamp(), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo start"])), + T2 = timer:now_diff(os:timestamp(), T1), + {ok, "pong"} = sh(filename:join([OutputDir, "foo", "bin", "foo ping"])), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo stop"])), + ?assert((T2 div 1000) > 3000), + ok. + +mixed_custom_and_builtin_start_script_hooks(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + + rlx_test_utils:create_full_app(LibDir1, "goal_app", "0.0.1", + [stdlib,kernel], []), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + + ConfigFile = filename:join([LibDir1, "relx.config"]), + rlx_test_utils:write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app]}, + {lib_dirs, [filename:join(LibDir1, "*")]}, + {generate_start_script, true}, + {extended_start_script, true}, + {extended_start_script_hooks, [ + {pre_start, [ + {custom, "hooks/pre_start"} + ]}, + {post_start, [ + wait_for_vm_start, + {pid, filename:join([OutputDir, "foo.pid"])}, + {wait_for_process, goal_app_srv_signal}, + {custom, "hooks/post_start"} + ]}, + {pre_stop, [ + {custom, "hooks/pre_stop"} + ]}, + {post_stop, [ + {custom, "hooks/post_stop"} + ]} + ]}, + {mkdir, "scripts"}, + {overlay, [{copy, "./pre_start", "bin/hooks/pre_start"}, + {copy, "./post_start", "bin/hooks/post_start"}, + {copy, "./pre_stop", "bin/hooks/pre_stop"}, + {copy, "./post_stop", "bin/hooks/post_stop"}]} + ]), + + %% write the hook scripts, each of them will write an erlang term to a file + %% that will later be consulted + ok = file:write_file(filename:join([LibDir1, "./pre_start"]), + "#!/bin/bash\n# $*\necho \\{pre_start, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + ok = file:write_file(filename:join([LibDir1, "./post_start"]), + "#!/bin/bash\n# $*\necho \\{post_start, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + ok = file:write_file(filename:join([LibDir1, "./pre_stop"]), + "#!/bin/bash\n# $*\necho \\{pre_stop, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + ok = file:write_file(filename:join([LibDir1, "./post_stop"]), + "#!/bin/bash\n# $*\necho \\{post_stop, $REL_NAME, \\'$NAME\\', $COOKIE\\}. >> test"), + + {ok, _State} = relx:do(foo, undefined, [], [LibDir1], 3, + OutputDir, ConfigFile), + %% now start/stop the release to make sure the script hooks are really getting + %% executed + %% get the current time, we'll measure how long it took for the node to + %% start, it must be at least 3 seconds which is the time it takes the + %% goal_app_srv to register the signal + T1 = os:timestamp(), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo start"])), + % this run doesn't need the sleep because the wait_for_vm_start + % start script makes it unnecessary + T2 = timer:now_diff(os:timestamp(), T1), + {ok, "pong"} = sh(filename:join([OutputDir, "foo", "bin", "foo ping"])), + ?assert((T2 div 1000) > 3000), + %% check that the pid file really was created + ?assert(ec_file:exists(filename:join([OutputDir, "foo.pid"]))), + os:cmd(filename:join([OutputDir, "foo", "bin", "foo stop"])), + %% now check that the output file contains the expected format + {ok,[{pre_start, foo, _, foo}, + {post_start, foo, _, foo}, + {pre_stop, foo, _, foo}, + {post_stop, foo, _, foo}]} = file:consult(filename:join([OutputDir, "foo", "test"])). + %%%=================================================================== %%% Helper Functions %%%=================================================================== diff --git a/test/rlx_test_utils.erl b/test/rlx_test_utils.erl index 3ddc134..f120c75 100644 --- a/test/rlx_test_utils.erl +++ b/test/rlx_test_utils.erl @@ -12,6 +12,13 @@ create_app(Dir, Name, Vsn, Deps, LibDeps) -> rlx_app_info:new(erlang:list_to_atom(Name), Vsn, AppDir, Deps, []). +create_full_app(Dir, Name, Vsn, Deps, LibDeps) -> + AppDir = filename:join([Dir, Name ++ "-" ++ Vsn]), + write_full_app_files(AppDir, Name, Vsn, Deps, LibDeps), + compile_src_files(AppDir), + rlx_app_info:new(erlang:list_to_atom(Name), Vsn, AppDir, + Deps, []). + create_empty_app(Dir, Name, Vsn, Deps, LibDeps) -> AppDir = filename:join([Dir, Name ++ "-" ++ Vsn]), write_app_file(AppDir, Name, Vsn, Deps, LibDeps), @@ -50,6 +57,97 @@ get_app_metadata(Name, Vsn, Deps, LibDeps) -> {registered, []}, {applications, Deps}]}. +write_full_app_files(Dir, Name, Vsn, Deps, LibDeps) -> + %% write out the .app file + AppFilename = filename:join([Dir, "ebin", Name ++ ".app"]), + ok = filelib:ensure_dir(AppFilename), + ok = ec_file:write_term(AppFilename, + get_full_app_metadata(Name, Vsn, Deps, LibDeps)), + %% write out the _app.erl file + ApplicationFilename = filename:join([Dir, "src", Name ++ "_app.erl"]), + ok = filelib:ensure_dir(ApplicationFilename), + ok = file:write_file(ApplicationFilename, full_application_contents(Name)), + %% write out the supervisor + SupervisorFilename = filename:join([Dir, "src", Name ++ "_sup.erl"]), + ok = filelib:ensure_dir(SupervisorFilename), + ok = file:write_file(SupervisorFilename, supervisor_contents(Name)), + %% and finally the gen_server + GenServerFilename = filename:join([Dir, "src", Name ++ "_srv.erl"]), + ok = filelib:ensure_dir(GenServerFilename), + ok = file:write_file(GenServerFilename, gen_server_contents(Name)), + ok. + +compile_src_files(Dir) -> + %% compile all *.erl files in src to ebin + SrcDir = filename:join([Dir, "src"]), + OutputDir = filename:join([Dir, "ebin"]), + lists:foreach(fun(SrcFile) -> + {ok, _} = compile:file(SrcFile, [{outdir, OutputDir}, + return_errors]) + end, ec_file:find(SrcDir, "\\.erl")), + ok. + +get_full_app_metadata(Name, Vsn, Deps, LibDeps) -> + {application, erlang:list_to_atom(Name), + [{description, ""}, + {vsn, Vsn}, + {modules, [goal_app_app,goal_app_sup,goal_app_srv]}, + {mod, {erlang:list_to_atom(Name ++ "_app"), + []}}, + {included_applications, LibDeps}, + {registered, []}, + {applications, Deps}]}. + +full_application_contents(Name) -> + "-module("++Name++"_app).\n" + "-behaviour(application).\n" + "-export([start/2, stop/1]).\n" + "start(_StartType, _StartArgs) ->\n" + " "++Name++"_sup:start_link().\n" + "stop(_State) ->\n" + " ok.\n". + +supervisor_contents(Name) -> + "-module("++Name++"_sup).\n" + "-behaviour(supervisor).\n" + "-export([start_link/0]).\n" + "-export([init/1]).\n" + "-define(SERVER, ?MODULE).\n" + "start_link() ->\n" + " supervisor:start_link({local, ?SERVER}, ?MODULE, []).\n" + "init([]) ->\n" + " {ok, { {one_for_all, 0, 1},\n" + " [{"++Name++"_srv, {"++Name++"_srv, start_link, []},\n" + " transient, 5000, worker, ["++Name++"_srv]}\n" + " ]\n" + " }}.\n". + +gen_server_contents(Name) -> + "-module("++Name++"_srv).\n" + "-behaviour(gen_server).\n" + "-record(state, {}).\n" + "-export([start_link/0]).\n" + "-export([init/1,handle_call/3,handle_cast/2,\n" + " handle_info/2,terminate/2,code_change/3]).\n" + "start_link() ->\n" + " gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).\n" + "init([]) ->\n" + " erlang:send_after(4000, self(), register_signal)," + " {ok, #state{}}.\n" + "handle_call(_Event, _From, State) ->\n" + " {reply, ok, State}.\n" + "handle_cast(_Event, State) ->\n" + " {noreply, State}.\n" + "handle_info(register_signal, State) ->\n" + " erlang:register(goal_app_srv_signal, spawn(fun() -> timer:sleep(200000) end)),\n" + " {noreply, State};\n" + "handle_info(_Info, State) ->\n" + " {noreply, State}.\n" + "terminate(_Reason, _State) ->\n" + " ok.\n" + "code_change(_OldVsn, State, _Extra) ->\n" + " {ok, State}.\n". + create_random_name(Name) -> Name ++ erlang:integer_to_list(random_uniform(1000000)). @@ -113,4 +211,4 @@ list_to_term(String) -> unescape_string(String) -> re:replace(String, "\"", "", - [global, {return, list}]). \ No newline at end of file + [global, {return, list}]). -- cgit v1.2.3