%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2010. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
-module(heart).
%%%--------------------------------------------------------------------
%%% This is a rewrite of pre_heart from BS.3.
%%%
%%% The purpose of this process-module is to act as an supervisor
%%% of the entire erlang-system. This 'heart' beats with a frequence
%%% satisfying an external port program *not* reboot the entire
%%% system. If however the erlang-emulator would hang, a reboot is
%%% then needed.
%%%
%%% It recognizes the flag '-heart'
%%%--------------------------------------------------------------------
-export([start/0, init/2, set_cmd/1, clear_cmd/0, get_cmd/0, cycle/0]).
-define(START_ACK, 1).
-define(HEART_BEAT, 2).
-define(SHUT_DOWN, 3).
-define(SET_CMD, 4).
-define(CLEAR_CMD, 5).
-define(GET_CMD, 6).
-define(HEART_CMD, 7).
-define(TIMEOUT, 5000).
-define(CYCLE_TIMEOUT, 10000).
%%---------------------------------------------------------------------
-spec start() -> 'ignore' | {'error', term()} | {'ok', pid()}.
start() ->
case whereis(heart) of
undefined ->
%% As heart survives a init:restart/0 the Parent
%% of heart must be init.
%% The init process is responsible to create a link
%% to heart.
Pid = spawn(?MODULE, init, [self(), whereis(init)]),
wait_for_init_ack(Pid);
Pid ->
{ok, Pid}
end.
wait_for_init_ack(From) ->
receive
{ok, From} = Ok ->
Ok;
{no_heart, From} ->
ignore;
{Error, From} ->
{error, Error}
end.
-spec init(pid(), pid()) -> {'no_heart', pid()} | {'start_error', pid()}.
init(Starter, Parent) ->
process_flag(trap_exit, true),
process_flag(priority, max),
register(heart, self()),
case catch start_portprogram() of
{ok, Port} ->
Starter ! {ok, self()},
loop(Parent, Port, "");
no_heart ->
Starter ! {no_heart, self()};
error ->
Starter ! {start_error, self()}
end.
-spec set_cmd(string()) -> 'ok' | {'error', {'bad_cmd', string()}}.
set_cmd(Cmd) ->
heart ! {self(), set_cmd, Cmd},
wait().
-spec get_cmd() -> 'ok'.
get_cmd() ->
heart ! {self(), get_cmd},
wait().
-spec clear_cmd() -> {'ok', string()}.
clear_cmd() ->
heart ! {self(), clear_cmd},
wait().
%%% Should be used solely by the release handler!!!!!!!
-spec cycle() -> 'ok' | {'error', term()}.
cycle() ->
heart ! {self(), cycle},
wait().
wait() ->
receive
{heart, Res} ->
Res
end.
start_portprogram() ->
check_start_heart(),
HeartCmd = "heart -pid " ++ os:getpid() ++ " " ++ get_heart_timeouts(),
try open_port({spawn, HeartCmd}, [{packet, 2}]) of
Port when is_port(Port) ->
case wait_ack(Port) of
ok ->
{ok, Port};
{error, Reason} ->
report_problem({{port_problem, Reason},
{heart, start_portprogram, []}}),
error
end
catch
_:Reason ->
report_problem({{open_port, Reason},
{heart, start_portprogram, []}}),
error
end.
get_heart_timeouts() ->
HeartOpts = case os:getenv("HEART_BEAT_TIMEOUT") of
false -> "";
H when is_list(H) ->
"-ht " ++ H
end,
HeartOpts ++ case os:getenv("HEART_BEAT_BOOT_DELAY") of
false -> "";
W when is_list(W) ->
" -wt " ++ W
end.
check_start_heart() ->
case init:get_argument(heart) of
{ok, [[]]} ->
ok;
error ->
throw(no_heart);
{ok, [[X|_]|_]} ->
report_problem({{bad_heart_flag, list_to_atom(X)},
{heart, check_start_heart, []}}),
throw(error)
end.
wait_ack(Port) ->
receive
{Port, {data, [?START_ACK]}} ->
ok;
{'EXIT', Port, badsig} -> % Since this is not synchronous, skip it!
wait_ack(Port);
{'EXIT', Port, Reason} -> % The port really terminated.
{error, Reason}
end.
loop(Parent, Port, Cmd) ->
send_heart_beat(Port),
receive
{From, set_cmd, NewCmd} when length(NewCmd) < 2047 ->
send_heart_cmd(Port, NewCmd),
wait_ack(Port),
From ! {heart, ok},
loop(Parent, Port, NewCmd);
{From, set_cmd, NewCmd} ->
From ! {heart, {error, {bad_cmd, NewCmd}}},
loop(Parent, Port, Cmd);
{From, clear_cmd} ->
From ! {heart, ok},
send_heart_cmd(Port, ""),
wait_ack(Port),
loop(Parent, Port, "");
{From, get_cmd} ->
From ! {heart, get_heart_cmd(Port)},
loop(Parent, Port, Cmd);
{From, cycle} ->
%% Calls back to loop
do_cycle_port_program(From, Parent, Port, Cmd);
{'EXIT', Parent, shutdown} ->
no_reboot_shutdown(Port);
{'EXIT', Parent, Reason} ->
exit(Port, Reason),
exit(Reason);
{'EXIT', Port, badsig} -> % we can ignore badsig-messages!
loop(Parent, Port, Cmd);
{'EXIT', Port, _Reason} ->
exit({port_terminated, {heart, loop, [Parent, Port, Cmd]}});
_ ->
loop(Parent, Port, Cmd)
after
?TIMEOUT ->
loop(Parent, Port, Cmd)
end.
-spec no_reboot_shutdown(port()) -> no_return().
no_reboot_shutdown(Port) ->
send_shutdown(Port),
receive
{'EXIT', Port, Reason} when Reason =/= badsig ->
exit(normal)
end.
do_cycle_port_program(Caller, Parent, Port, Cmd) ->
case catch start_portprogram() of
{ok, NewPort} ->
send_shutdown(Port),
receive
{'EXIT', Port, _Reason} ->
send_heart_cmd(NewPort, Cmd),
Caller ! {heart, ok},
loop(Parent, NewPort, Cmd)
after
?CYCLE_TIMEOUT ->
%% Huh! Two heart port programs running...
%% well, the old one has to be sick not to respond
%% so we'll settle for the new one...
send_heart_cmd(NewPort, Cmd),
Caller ! {heart, {error, stop_error}},
loop(Parent, NewPort, Cmd)
end;
no_heart ->
Caller ! {heart, {error, no_heart}},
loop(Parent, Port, Cmd);
error ->
Caller ! {heart, {error, start_error}},
loop(Parent, Port, Cmd)
end.
%% "Beates" the heart once.
send_heart_beat(Port) -> Port ! {self(), {command, [?HEART_BEAT]}}.
%% Set a new HEART_COMMAND.
send_heart_cmd(Port, []) ->
Port ! {self(), {command, [?CLEAR_CMD]}};
send_heart_cmd(Port, Cmd) ->
Port ! {self(), {command, [?SET_CMD|Cmd]}}.
get_heart_cmd(Port) ->
Port ! {self(), {command, [?GET_CMD]}},
receive
{Port, {data, [?HEART_CMD | Cmd]}} ->
{ok, Cmd}
end.
%% Sends shutdown command to the port.
send_shutdown(Port) -> Port ! {self(), {command, [?SHUT_DOWN]}}.
%% We must report using erlang:display/1 since we don't know whether
%% there is an error_logger available or not.
report_problem(Error) ->
erlang:display(Error).