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. --- test/rlx_extended_bin_SUITE.erl | 248 +++++++++++++++++++++++++++++++++++++++- test/rlx_test_utils.erl | 100 +++++++++++++++- 2 files changed, 343 insertions(+), 5 deletions(-) (limited to 'test') 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