aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xpriv/templates/extended_bin77
-rw-r--r--src/rlx_topo.erl189
-rw-r--r--test/rlx_extended_bin_SUITE.erl140
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 &lt; 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
%%%===================================================================