diff options
-rwxr-xr-x | priv/templates/extended_bin | 77 | ||||
-rw-r--r-- | src/rlx_topo.erl | 189 | ||||
-rw-r--r-- | test/rlx_extended_bin_SUITE.erl | 140 |
3 files changed, 279 insertions, 127 deletions
diff --git a/priv/templates/extended_bin b/priv/templates/extended_bin index 2580dcc..0abf38b 100755 --- a/priv/templates/extended_bin +++ b/priv/templates/extended_bin @@ -120,7 +120,7 @@ find_erts_dir() { else __erl="$(which erl)" code="io:format(\"~s\", [code:root_dir()]), halt()." - __erl_root="$("$__erl" -boot no_dot_erlang -noshell -eval "$code")" + __erl_root="$("$__erl" -boot no_dot_erlang -sasl errlog_type error -noshell -eval "$code")" ERTS_DIR="$__erl_root/erts-$ERTS_VSN" ROOTDIR="$__erl_root" fi @@ -294,16 +294,77 @@ export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH" ERTS_LIB_DIR="$(dirname "$ERTS_DIR")/lib" VMARGS_PATH=$(add_path vm.args $VMARGS_PATH) -# Extract the target node name from node.args -NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH" || true) + +# Check vm.args and other files referenced via -args_file parameters for: +# - nonexisting -args_files +# - circular dependencies of -args_files +# - relative paths in -args_file parameters +# - multiple/mixed occurences of -name and -sname parameters +# - missing -name or -sname parameters +# If all checks pass, extract the target node name +set +e +TMP_NAME_ARG=$(awk 'function check_name(file) +{ + if (system("test -f "file)) { + print file" not found" + exit 3 + } + if (system("test -r "file)) { + print file" not readable" + exit 3 + } + while ((getline line<file)>0) { + if (line~/^-args_file +/) { + gsub(/^-args_file +| *$/, "", line) + if (!(line~/^\//)) { + print "relative path "line" encountered in "file + exit 4 + } + if (line in files) { + print "circular reference to "line" encountered in "file + exit 5 + } + files[line]=line + check_name(line) + } + else if (line~/^-s?name +/) { + if (name!="") { + print "\""line"\" parameter found in "file" but already specified as \""name"\"" + exit 2 + } + name=line + } + } +} + +BEGIN { + split("", files) + name="" +} + +{ + files[FILENAME]=FILENAME + check_name(FILENAME) + if (name=="") { + print "need to have exactly one of either -name or -sname parameters but none found" + exit 1 + } + print name + exit 0 +}' "$VMARGS_PATH") +TMP_NAME_ARG_RC=$? +case $TMP_NAME_ARG_RC in + 0) NAME_ARG="$TMP_NAME_ARG";; + *) echo "$TMP_NAME_ARG" + exit $TMP_NAME_ARG_RC;; +esac +unset TMP_NAME_ARG +unset TMP_NAME_ARG_RC +set -e + # Perform replacement of variables in ${NAME_ARG} NAME_ARG=$(eval echo "${NAME_ARG}") -if [ -z "$NAME_ARG" ]; then - echo "vm.args needs to have either -name or -sname parameter." - exit 1 -fi - # Extract the name type and name from the NAME_ARG for REMSH NAME_TYPE="$(echo "$NAME_ARG" | awk '{print $1}')" NAME="$(echo "$NAME_ARG" | awk '{print $2}')" diff --git a/src/rlx_topo.erl b/src/rlx_topo.erl index b9c94b1..f8fc5ad 100644 --- a/src/rlx_topo.erl +++ b/src/rlx_topo.erl @@ -17,10 +17,12 @@ %%%------------------------------------------------------------------- %%% @author Joe Armstrong %%% @author Eric Merritt +%%% @author Konstantin Tcepliaev %%% @doc %%% This is a pretty simple topological sort for erlang. It was %%% originally written for ermake by Joe Armstrong back in '98. It %%% has been pretty heavily modified by Eric Merritt since '06 and modified again for Relx. +%%% Konstantin Tcepliaev rewrote the algorithm in 2017. %%% %%% A partial order on the set S is a set of pairs {Xi,Xj} such that %%% some relation between Xi and Xj is obeyed. @@ -28,24 +30,22 @@ %%% A topological sort of a partial order is a sequence of elements %%% [X1, X2, X3 ...] such that if whenever {Xi, Xj} is in the partial %%% order i < j +%%% +%%% This particular implementation guarantees that nodes closer to +%%% the top level of the graph will be put as close as possible to +%%% the beginning of the resulting list - this ensures that dependencies +%%% are started as late as possible, and top-level apps are started +%%% as early as possible. %%% @end %%%------------------------------------------------------------------- -module(rlx_topo). --export([sort/1, - sort_apps/1, +-export([sort_apps/1, format_error/1]). -include("relx.hrl"). %%==================================================================== -%% Types -%%==================================================================== --type pair() :: {DependentApp::atom(), PrimaryApp::atom()}. --type name() :: AppName::atom(). --type element() :: name() | pair(). - -%%==================================================================== %% API %%==================================================================== @@ -58,37 +58,64 @@ {ok, [rlx_app_info:t()]} | relx:error(). sort_apps(Apps) -> - Pairs = apps_to_pairs(Apps), - case sort(Pairs) of - {ok, Names} -> - {ok, names_to_apps(Names, Apps)}; + AppDeps = [{rlx_app_info:name(App), + rlx_app_info:active_deps(App) ++ rlx_app_info:library_deps(App)} + || App <- Apps], + {AppNames, _} = lists:unzip(AppDeps), + case lists:foldl(fun iterator/2, {ok, [], AppDeps, []}, AppNames) of + {ok, Names, _, _} -> + {ok, names_to_apps(lists:reverse(Names), Apps)}; E -> E end. -%% @doc Do a topological sort on the list of pairs. --spec sort([pair()]) -> {ok, [atom()]} | relx:error(). -sort(Pairs) -> - iterate(Pairs, [], all(Pairs)). - %% @doc nicely format the error from the sort. -spec format_error(Reason::term()) -> iolist(). -format_error({cycle, Pairs}) -> +format_error({cycle, App, Path}) -> ["Cycle detected in dependency graph, this must be resolved " "before we can continue:\n", - case Pairs of - [{P1, P2}] -> - [rlx_util:indent(2), erlang:atom_to_list(P2), "->", erlang:atom_to_list(P1)]; - [{P1, P2} | Rest] -> - [rlx_util:indent(2), erlang:atom_to_list(P2), "->", erlang:atom_to_list(P1), - [["-> ", erlang:atom_to_list(PP2), " -> ", erlang:atom_to_list(PP1)] || {PP1, PP2} <- Rest]]; - [] -> - [] - end]. + rlx_util:indent(2), + [[erlang:atom_to_list(A), " -> "] || A <- lists:reverse(Path)], + erlang:atom_to_list(App)]. %%==================================================================== %% Internal Functions %%==================================================================== + +-type name() :: AppName::atom(). +-type app_dep() :: {AppName::name(), [DepName::name()]}. +-type iterator_state() :: {ok, [Acc::name()], + [Apps::app_dep()], + [Path::name()]}. + +-spec iterator(name(), iterator_state() | relx:error()) -> + iterator_state() | relx:error(). +iterator(App, {ok, Acc, Apps, Path}) -> + case lists:member(App, Acc) of + false -> + %% haven't seen this app yet + case lists:keytake(App, 1, Apps) of + {value, {App, Deps}, NewApps} -> + DepInit = {ok, Acc, NewApps, [App | Path]}, + %% recurse over deps + case lists:foldl(fun iterator/2, DepInit, Deps) of + {ok, DepAcc, DepApps, _} -> + {ok, [App | DepAcc], DepApps, Path}; + Error -> + Error + end; + false -> + %% we have visited this app before, + %% that means there's a cycle + ?RLX_ERROR({cycle, App, Path}) + end; + true -> + %% this app and its deps were already processed + {ok, Acc, Apps, Path} + end; +iterator(_, Error) -> + Error. + -spec names_to_apps([atom()], [rlx_app_info:t()]) -> [rlx_app_info:t()]. names_to_apps(Names, Apps) -> [find_app_by_name(Name, Apps) || Name <- Names]. @@ -101,104 +128,17 @@ find_app_by_name(Name, Apps) -> end, Apps), App1. --spec apps_to_pairs([rlx_app_info:t()]) -> [pair()]. -apps_to_pairs(Apps) -> - lists:flatten([app_to_pairs(App) || App <- Apps]). - --spec app_to_pairs(rlx_app_info:t()) -> [pair()]. -app_to_pairs(App) -> - [{DepApp, rlx_app_info:name(App)} || - DepApp <- - rlx_app_info:active_deps(App) ++ - rlx_app_info:library_deps(App)]. - - -%% @doc Iterate over the system. @private --spec iterate([pair()], [name()], [name()]) -> - {ok, [name()]} | relx:error(). -iterate([], L, All) -> - {ok, remove_duplicates(L ++ subtract(All, L))}; -iterate(Pairs, L, All) -> - case subtract(lhs(Pairs), rhs(Pairs)) of - [] -> - ?RLX_ERROR({cycle, Pairs}); - Lhs -> - iterate(remove_pairs(Lhs, Pairs), L ++ Lhs, All) - end. - --spec all([pair()]) -> [atom()]. -all(L) -> - lhs(L) ++ rhs(L). - --spec lhs([pair()]) -> [atom()]. -lhs(L) -> - [X || {X, _} <- L]. - --spec rhs([pair()]) -> [atom()]. -rhs(L) -> - [Y || {_, Y} <- L]. - -%% @doc all the elements in L1 which are not in L2 -%% @private --spec subtract([element()], [element()]) -> [element()]. -subtract(L1, L2) -> - [X || X <- L1, not lists:member(X, L2)]. - -%% @doc remove dups from the list. @private --spec remove_duplicates([element()]) -> [element()]. -remove_duplicates([H|T]) -> - case lists:member(H, T) of - true -> - remove_duplicates(T); - false -> - [H|remove_duplicates(T)] - end; -remove_duplicates([]) -> - []. - -%% @doc -%% removes all pairs from L2 where the first element -%% of each pair is a member of L1 -%% -%% L2' L1 = [X] L2 = [{X,Y}]. -%% @private --spec remove_pairs([atom()], [pair()]) -> [pair()]. -remove_pairs(L1, L2) -> - [All || All={X, _Y} <- L2, not lists:member(X, L1)]. - %%==================================================================== %% Tests %%==================================================================== -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -topo_1_test() -> - Pairs = [{one,two},{two,four},{four,six}, - {two,ten},{four,eight}, - {six,three},{one,three}, - {three,five},{five,eight}, - {seven,five},{seven,nine}, - {nine,four},{nine,ten}], - ?assertMatch({ok, [one,seven,two,nine,four,six,three,five,eight,ten]}, - sort(Pairs)). -topo_2_test() -> - Pairs = [{app2, app1}, {zapp1, app1}, {stdlib, app1}, - {app3, app2}, {kernel, app1}, {kernel, app3}, - {app2, zapp1}, {app3, zapp1}, {zapp2, zapp1}], - ?assertMatch({ok, [stdlib, kernel, zapp2, - app3, app2, zapp1, app1]}, - sort(Pairs)). - -topo_pairs_cycle_test() -> - Pairs = [{app2, app1}, {app1, app2}, {stdlib, app1}], - ?assertMatch({error, {_, {cycle, [{app2, app1}, {app1, app2}]}}}, - sort(Pairs)). - topo_apps_cycle_test() -> {ok, App1} = rlx_app_info:new(app1, "0.1", "/no-dir", [app2], [stdlib]), {ok, App2} = rlx_app_info:new(app2, "0.1", "/no-dir", [app1], []), Apps = [App1, App2], - ?assertMatch({error, {_, {cycle, [{app2,app1},{app1,app2}]}}}, + ?assertMatch({error, {_, {cycle, app1, [app2, app1]}}}, sort_apps(Apps)). topo_apps_good_test() -> @@ -212,8 +152,21 @@ topo_apps_good_test() -> rlx_app_info:new(kernel, "0.1", "/no-dir", [], []), rlx_app_info:new(zapp2, "0.1", "/no-dir", [], [])]], {ok, Sorted} = sort_apps(Apps), - ?assertMatch([stdlib, kernel, zapp2, - app3, app2, zapp1, app1], + ?assertMatch([kernel, app3, app2, zapp2, zapp1, stdlib, app1], + [rlx_app_info:name(App) || App <- Sorted]). + +topo_apps_1_test() -> + Apps = [App || + {ok, App} <- + [rlx_app_info:new(app0, "0.1", "/no-dir", [], [stdlib, dep1, dep2, dep3]), + rlx_app_info:new(app1, "0.1", "/no-dir", [], [stdlib, kernel]), + rlx_app_info:new(dep1, "0.1", "/no-dir", [], []), + rlx_app_info:new(dep2, "0.1", "/no-dir", [], []), + rlx_app_info:new(dep3, "0.1", "/no-dir", [], []), + rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []), + rlx_app_info:new(kernel, "0.1", "/no-dir", [], [])]], + {ok, Sorted} = sort_apps(Apps), + ?assertMatch([stdlib, dep1, dep2, dep3, app0, kernel, app1], [rlx_app_info:name(App) || App <- Sorted]). topo_apps_2_test() -> diff --git a/test/rlx_extended_bin_SUITE.erl b/test/rlx_extended_bin_SUITE.erl index a9a7df7..c2e6bc2 100644 --- a/test/rlx_extended_bin_SUITE.erl +++ b/test/rlx_extended_bin_SUITE.erl @@ -22,6 +22,13 @@ end_per_suite/1, init_per_testcase/2, all/0, + start_sname_in_other_argsfile/1, + start_fail_when_no_name/1, + start_fail_when_multiple_names/1, + start_fail_when_missing_argsfile/1, + start_fail_when_nonreadable_argsfile/1, + start_fail_when_relative_argsfile/1, + start_fail_when_circular_argsfiles/1, ping/1, shortname_ping/1, longname_ping/1, @@ -67,7 +74,10 @@ init_per_testcase(_, Config) -> {state, State1} | Config]. all() -> - [ping, shortname_ping, longname_ping, attach, pid, restart, reboot, escript, + [start_sname_in_other_argsfile, start_fail_when_no_name, start_fail_when_multiple_names, + start_fail_when_missing_argsfile, start_fail_when_nonreadable_argsfile, + start_fail_when_relative_argsfile, start_fail_when_circular_argsfiles, + ping, shortname_ping, longname_ping, attach, pid, restart, reboot, escript, remote_console, replace_os_vars, replace_os_vars_multi_node, replace_os_vars_included_config, replace_os_vars_custom_location, 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, @@ -1387,6 +1397,134 @@ custom_status_script(Config) -> {ok, [Status]} = file:consult(filename:join([OutputDir, "status.txt"])), {ok, {status, foo, _, foo} = Status}. +start_sname_in_other_argsfile(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"]), + VmArgs = filename:join([LibDir1, "vm.args"]), + VmArgs2 = VmArgs ++ ".2", + + rlx_test_utils:write_config(ConfigFile, + [{release, {foo, "0.0.1"}, + [goal_app]}, + {lib_dirs, [filename:join(LibDir1, "*")]}, + {vm_args, VmArgs}, + {generate_start_script, true}, + {extended_start_script, true} + ]), + + ec_file:write(VmArgs, "-args_file " ++ VmArgs2 ++ "\n\n" + "-setcookie cookie\n"), + + ec_file:write(VmArgs2, "-sname foo\n"), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + + {ok, _State} = relx:do([{relname, foo}, + {relvsn, "0.0.1"}, + {goals, []}, + {lib_dirs, [LibDir1]}, + {log_level, 3}, + {output_dir, OutputDir}, + {config, ConfigFile}], ["release"]), + + %% now start/stop the release to make sure the extended script is working + {ok, _} = sh(filename:join([OutputDir, "foo", "bin", "foo start"])), + timer:sleep(2000), + {ok, "pong"} = sh(filename:join([OutputDir, "foo", "bin", "foo ping"])), + {ok, _} = sh(filename:join([OutputDir, "foo", "bin", "foo stop"])), + %% a ping should fail after stopping a node + {error, 1, _} = sh(filename:join([OutputDir, "foo", "bin", "foo ping"])). + +start_fail_when_no_name(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + VmArgs = filename:join([LibDir1, "vm.args"]), + ec_file:write(VmArgs, "-setcookie cookie\n"), + start_fail_with_vmargs(Config, VmArgs, 1). + +start_fail_when_multiple_names(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + VmArgs = filename:join([LibDir1, "vm.args"]), + ec_file:write(VmArgs, "-name foo\n\n" + "-name bar\n\n" + "-setcookie cookie\n"), + start_fail_with_vmargs(Config, VmArgs, 2). + +start_fail_when_missing_argsfile(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + VmArgs = filename:join([LibDir1, "vm.args"]), + ec_file:write(VmArgs, "-name foo\n\n" + "-args_file " ++ VmArgs ++ ".nonexistent\n\n" + "-setcookie cookie\n"), + start_fail_with_vmargs(Config, VmArgs, 3). + +start_fail_when_nonreadable_argsfile(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + VmArgs = filename:join([LibDir1, "vm.args"]), + VmArgs2 = VmArgs ++ ".nonreadable", + ec_file:write(VmArgs, "-name foo\n\n" + "-args_file " ++ VmArgs2 ++ "\n\n" + "-setcookie cookie\n"), + ec_file:write(VmArgs2, ""), + file:change_mode(VmArgs2, 8#00333), + start_fail_with_vmargs(Config, VmArgs, 3). + +start_fail_when_relative_argsfile(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + VmArgs = filename:join([LibDir1, "vm.args"]), + ec_file:write(VmArgs, "-name foo\n\n" + "-args_file vm.args.relative\n\n" + "-setcookie cookie\n"), + start_fail_with_vmargs(Config, VmArgs, 4). + +start_fail_when_circular_argsfiles(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + VmArgs = filename:join([LibDir1, "vm.args"]), + VmArgs2 = VmArgs ++ ".2", + VmArgs3 = VmArgs ++ ".3", + ec_file:write(VmArgs, "-name foo\n\n" + "-args_file " ++ VmArgs2 ++ "\n\n" + "-setcookie cookie\n"), + ec_file:write(VmArgs2, "-args_file " ++ VmArgs3 ++ "\n"), + ec_file:write(VmArgs3, "-args_file " ++ VmArgs2 ++ "\n"), + start_fail_with_vmargs(Config, VmArgs, 5). + +%%------------------------------------------------------------------- +%% Helper Function for start_fail_when_* tests +%%------------------------------------------------------------------- +start_fail_with_vmargs(Config, VmArgs, ExpectedCode) -> + 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, "*")]}, + {vm_args, VmArgs}, + {generate_start_script, true}, + {extended_start_script, true} + ]), + + OutputDir = filename:join([proplists:get_value(priv_dir, Config), + rlx_test_utils:create_random_name("relx-output")]), + + {ok, _State} = relx:do([{relname, foo}, + {relvsn, "0.0.1"}, + {goals, []}, + {lib_dirs, [LibDir1]}, + {log_level, 3}, + {output_dir, OutputDir}, + {config, ConfigFile}], ["release"]), + + %% now start/stop the release to make sure the extended script is working + {error, ExpectedCode, _} = sh(filename:join([OutputDir, "foo", "bin", "foo start"])). + %%%=================================================================== %%% Helper Functions %%%=================================================================== |