|
|
@@ -4,9 +4,11 @@
|
|
|
%% ex: ft=erlang ts=4 sw=4 et
|
|
|
|
|
|
-define(TIMEOUT, 300000).
|
|
|
--define(INFO(Fmt,Args), io:format(standard_io, Fmt++"~n",Args)).
|
|
|
--define(ERROR(Fmt,Args), io:format(standard_error, "ERROR: "++Fmt++"~n",Args)).
|
|
|
--define(SEMVER_RE, <<"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[a-zA-Z\\d][-a-zA-Z.\\d]*)?(\\+[a-zA-Z\\d][-a-zA-Z.\\d]*)?$">>).
|
|
|
+-define(INFO(Fmt, Args), io:format(standard_io, Fmt ++ "~n", Args)).
|
|
|
+-define(ERROR(Fmt, Args), io:format(standard_error, "ERROR: " ++ Fmt ++ "~n", Args)).
|
|
|
+-define(SEMVER_RE,
|
|
|
+ <<"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[a-zA-Z\\d][-a-zA-Z.\\d]*)?(\\+[a-zA-Z\\d][-a-zA-Z.\\d]*)?$">>
|
|
|
+).
|
|
|
|
|
|
-mode(compile).
|
|
|
|
|
|
@@ -17,14 +19,15 @@ main([Command0, DistInfoStr | CommandArgs]) ->
|
|
|
%% convert arguments into a proplist
|
|
|
Opts = parse_arguments(CommandArgs),
|
|
|
%% invoke the command passed as argument
|
|
|
- F = case Command0 of
|
|
|
- "install" -> fun(A, B) -> install(A, B) end;
|
|
|
- "unpack" -> fun(A, B) -> unpack(A, B) end;
|
|
|
- "upgrade" -> fun(A, B) -> upgrade(A, B) end;
|
|
|
- "downgrade" -> fun(A, B) -> downgrade(A, B) end;
|
|
|
- "uninstall" -> fun(A, B) -> uninstall(A, B) end;
|
|
|
- "versions" -> fun(A, B) -> versions(A, B) end
|
|
|
- end,
|
|
|
+ F =
|
|
|
+ case Command0 of
|
|
|
+ "install" -> fun(A, B) -> install(A, B) end;
|
|
|
+ "unpack" -> fun(A, B) -> unpack(A, B) end;
|
|
|
+ "upgrade" -> fun(A, B) -> upgrade(A, B) end;
|
|
|
+ "downgrade" -> fun(A, B) -> downgrade(A, B) end;
|
|
|
+ "uninstall" -> fun(A, B) -> uninstall(A, B) end;
|
|
|
+ "versions" -> fun(A, B) -> versions(A, B) end
|
|
|
+ end,
|
|
|
F(DistInfo, Opts);
|
|
|
main(Args) ->
|
|
|
?INFO("unknown args: ~p", [Args]),
|
|
|
@@ -38,15 +41,15 @@ unpack({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
|
|
|
?INFO("Unpacked successfully: ~p", [Vsn]);
|
|
|
old ->
|
|
|
%% no need to unpack, has been installed previously
|
|
|
- ?INFO("Release ~s is marked old.",[Version]);
|
|
|
+ ?INFO("Release ~s is marked old.", [Version]);
|
|
|
unpacked ->
|
|
|
- ?INFO("Release ~s is already unpacked.",[Version]);
|
|
|
+ ?INFO("Release ~s is already unpacked.", [Version]);
|
|
|
current ->
|
|
|
- ?INFO("Release ~s is already installed and current.",[Version]);
|
|
|
+ ?INFO("Release ~s is already installed and current.", [Version]);
|
|
|
permanent ->
|
|
|
- ?INFO("Release ~s is already installed and set permanent.",[Version]);
|
|
|
+ ?INFO("Release ~s is already installed and set permanent.", [Version]);
|
|
|
{error, Reason} ->
|
|
|
- ?INFO("Unpack failed: ~p.",[Reason]),
|
|
|
+ ?INFO("Unpack failed: ~p.", [Reason]),
|
|
|
print_existing_versions(TargetNode),
|
|
|
erlang:halt(2)
|
|
|
end;
|
|
|
@@ -64,38 +67,46 @@ install({RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
|
|
|
maybe_permafy(TargetNode, RelName, Vsn, Opts);
|
|
|
old ->
|
|
|
%% no need to unpack, has been installed previously
|
|
|
- ?INFO("Release ~s is marked old, switching to it.",[Version]),
|
|
|
+ ?INFO("Release ~s is marked old, switching to it.", [Version]),
|
|
|
check_and_install(TargetNode, Version),
|
|
|
maybe_permafy(TargetNode, RelName, Version, Opts);
|
|
|
unpacked ->
|
|
|
- ?INFO("Release ~s is already unpacked, now installing.",[Version]),
|
|
|
+ ?INFO("Release ~s is already unpacked, now installing.", [Version]),
|
|
|
check_and_install(TargetNode, Version),
|
|
|
maybe_permafy(TargetNode, RelName, Version, Opts);
|
|
|
current ->
|
|
|
case proplists:get_value(permanent, Opts, true) of
|
|
|
true ->
|
|
|
- ?INFO("Release ~s is already installed and current, making permanent.",
|
|
|
- [Version]),
|
|
|
+ ?INFO(
|
|
|
+ "Release ~s is already installed and current, making permanent.",
|
|
|
+ [Version]
|
|
|
+ ),
|
|
|
permafy(TargetNode, RelName, Version);
|
|
|
false ->
|
|
|
- ?INFO("Release ~s is already installed and current.",
|
|
|
- [Version])
|
|
|
+ ?INFO(
|
|
|
+ "Release ~s is already installed and current.",
|
|
|
+ [Version]
|
|
|
+ )
|
|
|
end;
|
|
|
permanent ->
|
|
|
%% this release is marked permanent, however it might not the
|
|
|
%% one currently running
|
|
|
case current_release_version(TargetNode) of
|
|
|
Version ->
|
|
|
- ?INFO("Release ~s is already installed, running and set permanent.",
|
|
|
- [Version]);
|
|
|
+ ?INFO(
|
|
|
+ "Release ~s is already installed, running and set permanent.",
|
|
|
+ [Version]
|
|
|
+ );
|
|
|
CurrentVersion ->
|
|
|
- ?INFO("Release ~s is the currently running version.",
|
|
|
- [CurrentVersion]),
|
|
|
+ ?INFO(
|
|
|
+ "Release ~s is the currently running version.",
|
|
|
+ [CurrentVersion]
|
|
|
+ ),
|
|
|
check_and_install(TargetNode, Version),
|
|
|
maybe_permafy(TargetNode, RelName, Version, Opts)
|
|
|
end;
|
|
|
{error, Reason} ->
|
|
|
- ?INFO("Unpack failed: ~p",[Reason]),
|
|
|
+ ?INFO("Unpack failed: ~p", [Reason]),
|
|
|
print_existing_versions(TargetNode),
|
|
|
erlang:halt(2)
|
|
|
end;
|
|
|
@@ -119,8 +130,10 @@ uninstall({_RelName, NameTypeArg, NodeName, Cookie}, Opts) ->
|
|
|
?INFO("Release ~s is marked old, uninstalling it.", [Version]),
|
|
|
remove_release(TargetNode, Version);
|
|
|
unpacked ->
|
|
|
- ?INFO("Release ~s is marked unpacked, uninstalling it",
|
|
|
- [Version]),
|
|
|
+ ?INFO(
|
|
|
+ "Release ~s is marked unpacked, uninstalling it",
|
|
|
+ [Version]
|
|
|
+ ),
|
|
|
remove_release(TargetNode, Version);
|
|
|
current ->
|
|
|
?INFO("Uninstall failed: Release ~s is marked current.", [Version]),
|
|
|
@@ -140,10 +153,11 @@ parse_arguments(Args) ->
|
|
|
IsEnterprise = os:getenv("IS_ENTERPRISE") == "yes",
|
|
|
parse_arguments(Args, [{is_enterprise, IsEnterprise}]).
|
|
|
|
|
|
-parse_arguments([], Acc) -> Acc;
|
|
|
-parse_arguments(["--no-permanent"|Rest], Acc) ->
|
|
|
+parse_arguments([], Acc) ->
|
|
|
+ Acc;
|
|
|
+parse_arguments(["--no-permanent" | Rest], Acc) ->
|
|
|
parse_arguments(Rest, [{permanent, false}] ++ Acc);
|
|
|
-parse_arguments([VersionStr|Rest], Acc) ->
|
|
|
+parse_arguments([VersionStr | Rest], Acc) ->
|
|
|
Version = parse_version(VersionStr),
|
|
|
parse_arguments(Rest, [{version, Version}] ++ Acc).
|
|
|
|
|
|
@@ -162,18 +176,29 @@ unpack_release(RelName, TargetNode, Version, Opts) ->
|
|
|
{_, undefined} ->
|
|
|
{error, release_package_not_found};
|
|
|
{ReleasePackage, ReleasePackageLink} ->
|
|
|
- ?INFO("Release ~s not found, attempting to unpack ~s",
|
|
|
- [Version, ReleasePackage]),
|
|
|
- case rpc:call(TargetNode, release_handler, unpack_release,
|
|
|
- [ReleasePackageLink], ?TIMEOUT) of
|
|
|
- {ok, Vsn} -> {ok, Vsn};
|
|
|
+ ?INFO(
|
|
|
+ "Release ~s not found, attempting to unpack ~s",
|
|
|
+ [Version, ReleasePackage]
|
|
|
+ ),
|
|
|
+ case
|
|
|
+ rpc:call(
|
|
|
+ TargetNode,
|
|
|
+ release_handler,
|
|
|
+ unpack_release,
|
|
|
+ [ReleasePackageLink],
|
|
|
+ ?TIMEOUT
|
|
|
+ )
|
|
|
+ of
|
|
|
+ {ok, Vsn} ->
|
|
|
+ {ok, Vsn};
|
|
|
{error, {existing_release, Vsn}} ->
|
|
|
%% sometimes the user may have removed the release/<vsn> dir
|
|
|
%% for an `unpacked` release, then we need to re-unpack it from
|
|
|
%% the .tar ball
|
|
|
untar_for_unpacked_release(str(RelName), Vsn),
|
|
|
{ok, Vsn};
|
|
|
- {error, _} = Error -> Error
|
|
|
+ {error, _} = Error ->
|
|
|
+ Error
|
|
|
end
|
|
|
end;
|
|
|
Other ->
|
|
|
@@ -198,8 +223,8 @@ untar_for_unpacked_release(RelName, Vsn) ->
|
|
|
extract_tar(Cwd, Tar) ->
|
|
|
case erl_tar:extract(Tar, [keep_old_files, {cwd, Cwd}, compressed]) of
|
|
|
ok -> ok;
|
|
|
- {error, {Name, Reason}} -> % New erl_tar (R3A).
|
|
|
- throw({error, {cannot_extract_file, Name, Reason}})
|
|
|
+ % New erl_tar (R3A).
|
|
|
+ {error, {Name, Reason}} -> throw({error, {cannot_extract_file, Name, Reason}})
|
|
|
end.
|
|
|
|
|
|
%% 1. look for a release package tarball with the provided version:
|
|
|
@@ -217,8 +242,11 @@ find_and_link_release_package(Version, RelName, IsEnterprise) ->
|
|
|
ReleaseHandlerPackageLink = filename:join(Version, RelNameStr),
|
|
|
%% this is the symlink name we'll create once
|
|
|
%% we've found where the actual release package is located
|
|
|
- ReleaseLink = filename:join(["releases", Version,
|
|
|
- RelNameStr ++ ".tar.gz"]),
|
|
|
+ ReleaseLink = filename:join([
|
|
|
+ "releases",
|
|
|
+ Version,
|
|
|
+ RelNameStr ++ ".tar.gz"
|
|
|
+ ]),
|
|
|
ReleaseNamePattern =
|
|
|
case IsEnterprise of
|
|
|
false -> RelNameStr;
|
|
|
@@ -240,14 +268,18 @@ find_and_link_release_package(Version, RelName, IsEnterprise) ->
|
|
|
make_symlink_or_copy(filename:absname(Filename), ReleaseLink),
|
|
|
{Filename, ReleaseHandlerPackageLink};
|
|
|
Files ->
|
|
|
- ?ERROR("Found more than one package for version: '~s', "
|
|
|
- "files: ~p", [Version, Files]),
|
|
|
+ ?ERROR(
|
|
|
+ "Found more than one package for version: '~s', "
|
|
|
+ "files: ~p",
|
|
|
+ [Version, Files]
|
|
|
+ ),
|
|
|
erlang:halt(47)
|
|
|
end.
|
|
|
|
|
|
make_symlink_or_copy(Filename, ReleaseLink) ->
|
|
|
case file:make_symlink(Filename, ReleaseLink) of
|
|
|
- ok -> ok;
|
|
|
+ ok ->
|
|
|
+ ok;
|
|
|
{error, eexist} ->
|
|
|
?INFO("Symlink ~p already exists, recreate it", [ReleaseLink]),
|
|
|
ok = file:delete(ReleaseLink),
|
|
|
@@ -260,36 +292,55 @@ make_symlink_or_copy(Filename, ReleaseLink) ->
|
|
|
end.
|
|
|
|
|
|
parse_version(V) when is_list(V) ->
|
|
|
- hd(string:tokens(V,"/")).
|
|
|
+ hd(string:tokens(V, "/")).
|
|
|
|
|
|
check_and_install(TargetNode, Vsn) ->
|
|
|
%% Backup the sys.config, this will be used when we check and install release
|
|
|
%% NOTE: We cannot backup the old sys.config directly, because the
|
|
|
%% configs for plugins are only in app-envs, not in the old sys.config
|
|
|
Configs0 =
|
|
|
- [{AppName, rpc:call(TargetNode, application, get_all_env, [AppName], ?TIMEOUT)}
|
|
|
- || {AppName, _, _} <- rpc:call(TargetNode, application, which_applications, [], ?TIMEOUT)],
|
|
|
+ [
|
|
|
+ {AppName, rpc:call(TargetNode, application, get_all_env, [AppName], ?TIMEOUT)}
|
|
|
+ || {AppName, _, _} <- rpc:call(TargetNode, application, which_applications, [], ?TIMEOUT)
|
|
|
+ ],
|
|
|
Configs1 = [{AppName, Conf} || {AppName, Conf} <- Configs0, Conf =/= []],
|
|
|
- ok = file:write_file(filename:join(["releases", Vsn, "sys.config"]), io_lib:format("~p.", [Configs1])),
|
|
|
+ ok = file:write_file(
|
|
|
+ filename:join(["releases", Vsn, "sys.config"]), io_lib:format("~p.", [Configs1])
|
|
|
+ ),
|
|
|
|
|
|
%% check and install release
|
|
|
- case rpc:call(TargetNode, release_handler,
|
|
|
- check_install_release, [Vsn], ?TIMEOUT) of
|
|
|
+ case
|
|
|
+ rpc:call(
|
|
|
+ TargetNode,
|
|
|
+ release_handler,
|
|
|
+ check_install_release,
|
|
|
+ [Vsn],
|
|
|
+ ?TIMEOUT
|
|
|
+ )
|
|
|
+ of
|
|
|
{ok, _OtherVsn, _Desc} ->
|
|
|
ok;
|
|
|
{error, Reason} ->
|
|
|
?ERROR("Call release_handler:check_install_release failed: ~p.", [Reason]),
|
|
|
erlang:halt(3)
|
|
|
end,
|
|
|
- case rpc:call(TargetNode, release_handler, install_release,
|
|
|
- [Vsn, [{update_paths, true}]], ?TIMEOUT) of
|
|
|
+ case
|
|
|
+ rpc:call(
|
|
|
+ TargetNode,
|
|
|
+ release_handler,
|
|
|
+ install_release,
|
|
|
+ [Vsn, [{update_paths, true}]],
|
|
|
+ ?TIMEOUT
|
|
|
+ )
|
|
|
+ of
|
|
|
{ok, _, _} ->
|
|
|
?INFO("Installed Release: ~s.", [Vsn]),
|
|
|
ok;
|
|
|
{error, {no_such_release, Vsn}} ->
|
|
|
VerList =
|
|
|
iolist_to_binary(
|
|
|
- [io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]),
|
|
|
+ [io_lib:format("* ~s\t~s~n", [V, S]) || {V, S} <- which_releases(TargetNode)]
|
|
|
+ ),
|
|
|
?INFO("Installed versions:~n~s", [VerList]),
|
|
|
?ERROR("Unable to revert to '~s' - not installed.", [Vsn]),
|
|
|
erlang:halt(2);
|
|
|
@@ -298,11 +349,13 @@ check_and_install(TargetNode, Vsn) ->
|
|
|
%% If the value is soft_purge, release_handler:install_release/1
|
|
|
%% returns {error,{old_processes,Mod}}
|
|
|
{error, {old_processes, Mod}} ->
|
|
|
- ?ERROR("Unable to install '~s' - old processes still running code from module ~p",
|
|
|
- [Vsn, Mod]),
|
|
|
+ ?ERROR(
|
|
|
+ "Unable to install '~s' - old processes still running code from module ~p",
|
|
|
+ [Vsn, Mod]
|
|
|
+ ),
|
|
|
erlang:halt(3);
|
|
|
{error, Reason1} ->
|
|
|
- ?ERROR("Call release_handler:install_release failed: ~p",[Reason1]),
|
|
|
+ ?ERROR("Call release_handler:install_release failed: ~p", [Reason1]),
|
|
|
erlang:halt(4)
|
|
|
end.
|
|
|
|
|
|
@@ -310,22 +363,34 @@ maybe_permafy(TargetNode, RelName, Vsn, Opts) ->
|
|
|
case proplists:get_value(permanent, Opts, true) of
|
|
|
true ->
|
|
|
permafy(TargetNode, RelName, Vsn);
|
|
|
- false -> ok
|
|
|
+ false ->
|
|
|
+ ok
|
|
|
end.
|
|
|
|
|
|
permafy(TargetNode, RelName, Vsn) ->
|
|
|
RelNameStr = atom_to_list(RelName),
|
|
|
- ok = rpc:call(TargetNode, release_handler,
|
|
|
- make_permanent, [Vsn], ?TIMEOUT),
|
|
|
+ ok = rpc:call(
|
|
|
+ TargetNode,
|
|
|
+ release_handler,
|
|
|
+ make_permanent,
|
|
|
+ [Vsn],
|
|
|
+ ?TIMEOUT
|
|
|
+ ),
|
|
|
?INFO("Made release permanent: ~p", [Vsn]),
|
|
|
%% upgrade/downgrade the scripts by replacing them
|
|
|
- Scripts = [RelNameStr, RelNameStr++"_ctl", "nodetool", "install_upgrade.escript"],
|
|
|
- [{ok, _} = file:copy(filename:join(["bin", File++"-"++Vsn]),
|
|
|
- filename:join(["bin", File]))
|
|
|
- || File <- Scripts],
|
|
|
+ Scripts = [RelNameStr, RelNameStr ++ "_ctl", "nodetool", "install_upgrade.escript"],
|
|
|
+ [
|
|
|
+ {ok, _} = file:copy(
|
|
|
+ filename:join(["bin", File ++ "-" ++ Vsn]),
|
|
|
+ filename:join(["bin", File])
|
|
|
+ )
|
|
|
+ || File <- Scripts
|
|
|
+ ],
|
|
|
%% update the vars
|
|
|
UpdatedVars = io_lib:format("REL_VSN=\"~s\"~nERTS_VSN=\"~s\"~n", [Vsn, erts_vsn()]),
|
|
|
- file:write_file(filename:absname(filename:join(["releases", "emqx_vars"])), UpdatedVars, [append]).
|
|
|
+ file:write_file(filename:absname(filename:join(["releases", "emqx_vars"])), UpdatedVars, [
|
|
|
+ append
|
|
|
+ ]).
|
|
|
|
|
|
remove_release(TargetNode, Vsn) ->
|
|
|
case rpc:call(TargetNode, release_handler, remove_release, [Vsn], ?TIMEOUT) of
|
|
|
@@ -339,22 +404,31 @@ remove_release(TargetNode, Vsn) ->
|
|
|
|
|
|
which_releases(TargetNode) ->
|
|
|
R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT),
|
|
|
- [ {V, S} || {_,V,_, S} <- R ].
|
|
|
+ [{V, S} || {_, V, _, S} <- R].
|
|
|
|
|
|
%% the running release version is either the only one marked `current´
|
|
|
%% or, if none exists, the one marked `permanent`
|
|
|
current_release_version(TargetNode) ->
|
|
|
- R = rpc:call(TargetNode, release_handler, which_releases,
|
|
|
- [], ?TIMEOUT),
|
|
|
- Versions = [ {S, V} || {_,V,_, S} <- R ],
|
|
|
+ R = rpc:call(
|
|
|
+ TargetNode,
|
|
|
+ release_handler,
|
|
|
+ which_releases,
|
|
|
+ [],
|
|
|
+ ?TIMEOUT
|
|
|
+ ),
|
|
|
+ Versions = [{S, V} || {_, V, _, S} <- R],
|
|
|
%% current version takes priority over the permanent
|
|
|
- proplists:get_value(current, Versions,
|
|
|
- proplists:get_value(permanent, Versions)).
|
|
|
+ proplists:get_value(
|
|
|
+ current,
|
|
|
+ Versions,
|
|
|
+ proplists:get_value(permanent, Versions)
|
|
|
+ ).
|
|
|
|
|
|
print_existing_versions(TargetNode) ->
|
|
|
VerList = iolist_to_binary([
|
|
|
- io_lib:format("* ~s\t~s~n",[V,S])
|
|
|
- || {V,S} <- which_releases(TargetNode) ]),
|
|
|
+ io_lib:format("* ~s\t~s~n", [V, S])
|
|
|
+ || {V, S} <- which_releases(TargetNode)
|
|
|
+ ]),
|
|
|
?INFO("Installed versions:~n~s", [VerList]).
|
|
|
|
|
|
start_distribution(TargetNode, NameTypeArg, Cookie) ->
|
|
|
@@ -378,12 +452,12 @@ make_script_node(Node) ->
|
|
|
|
|
|
%% get name type from arg
|
|
|
get_name_type(NameTypeArg) ->
|
|
|
- case NameTypeArg of
|
|
|
- "-sname" ->
|
|
|
- shortnames;
|
|
|
- _ ->
|
|
|
- longnames
|
|
|
- end.
|
|
|
+ case NameTypeArg of
|
|
|
+ "-sname" ->
|
|
|
+ shortnames;
|
|
|
+ _ ->
|
|
|
+ longnames
|
|
|
+ end.
|
|
|
|
|
|
erts_vsn() ->
|
|
|
{ok, Str} = file:read_file(filename:join(["releases", "start_erl.data"])),
|
|
|
@@ -393,11 +467,14 @@ erts_vsn() ->
|
|
|
validate_target_version(TargetVersion, TargetNode) ->
|
|
|
CurrentVersion = current_release_version(TargetNode),
|
|
|
case {get_major_minor_vsn(CurrentVersion), get_major_minor_vsn(TargetVersion)} of
|
|
|
- {{Major, Minor}, {Major, Minor}} -> ok;
|
|
|
+ {{Major, Minor}, {Major, Minor}} ->
|
|
|
+ ok;
|
|
|
_ ->
|
|
|
- ?ERROR("Cannot upgrade/downgrade from '~s' to '~s'~n"
|
|
|
- "Hot upgrade is only supported between patch releases.",
|
|
|
- [CurrentVersion, TargetVersion]),
|
|
|
+ ?ERROR(
|
|
|
+ "Cannot upgrade/downgrade from '~s' to '~s'~n"
|
|
|
+ "Hot upgrade is only supported between patch releases.",
|
|
|
+ [CurrentVersion, TargetVersion]
|
|
|
+ ),
|
|
|
erlang:halt(48)
|
|
|
end.
|
|
|
|
|
|
@@ -409,7 +486,8 @@ get_major_minor_vsn(Version) ->
|
|
|
|
|
|
parse_semver(Version) ->
|
|
|
case re:run(Version, ?SEMVER_RE, [{capture, all_but_first, binary}]) of
|
|
|
- {match, Parts} -> Parts;
|
|
|
+ {match, Parts} ->
|
|
|
+ Parts;
|
|
|
nomatch ->
|
|
|
?ERROR("Invalid semantic version: '~s'~n", [Version]),
|
|
|
erlang:halt(22)
|