%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1996-2023. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% -module(gen). -compile({inline,[get_node/1]}). %%%----------------------------------------------------------------- %%% This module implements the really generic stuff of the generic %%% standard behaviours (e.g. gen_server, gen_fsm). %%% %%% The standard behaviour should export init_it/6. %%%----------------------------------------------------------------- -export([start/5, start/6, debug_options/2, hibernate_after/1, name/1, unregister_name/1, get_proc_name/1, get_parent/0, call/3, call/4, reply/2, send_request/3, send_request/5, wait_response/2, receive_response/2, check_response/2, wait_response/3, receive_response/3, check_response/3, reqids_new/0, reqids_size/1, reqids_add/3, reqids_to_list/1, stop/1, stop/3]). -export([init_it/6, init_it/7]). -export([format_status_header/2, format_status/4]). -export(['@wait_response_recv_opt'/3]). -define(MAX_INT_TIMEOUT, 4294967295). -define(default_timeout, 5000). -include("logger.hrl"). %%----------------------------------------------------------------- -export_type([reply_tag/0, request_id/0, request_id_collection/0]). -type linkage() :: 'monitor' | 'link' | 'nolink'. -type emgr_name() :: {'local', atom()} | {'global', term()} | {'via', Module :: module(), Name :: term()}. -type start_ret() :: {'ok', pid()} | {'ok', {pid(), reference()}} | 'ignore' | {'error', term()}. -type option() :: {'timeout', timeout()} | {'debug', [sys:debug_option()]} | {'hibernate_after', timeout()} | {'spawn_opt', [proc_lib:spawn_option()]}. -type server_ref() :: pid() | atom() | {atom(), node()} | {global, term()} | {via, module(), term()}. -opaque reply_tag() :: % As accepted by reply/2 reference() | nonempty_improper_list('alias', reference()) | nonempty_improper_list( nonempty_improper_list('alias', reference()), term()). -opaque request_id() :: reference(). -opaque request_id_collection() :: map(). -type response_timeout() :: 0..?MAX_INT_TIMEOUT | 'infinity' | {abs, integer()}. %%----------------------------------------------------------------- %% Starts a generic process. %% start(GenMod, LinkP, Mod, Args, Options) %% start(GenMod, LinkP, Name, Mod, Args, Options) %% GenMod = atom(), callback module implementing the 'real' fsm %% LinkP = link | nolink %% Name = {local, atom()} | {global, term()} | {via, atom(), term()} %% Args = term(), init arguments (to Mod:init/1) %% Options = [{timeout, Timeout} | {debug, [Flag]} | {spawn_opt, OptionList}] %% Flag = trace | log | {logfile, File} | statistics | debug %% (debug == log && statistics) %% Returns: {ok, Pid} | {ok, Pid, Reference} | ignore |{error, Reason} | %% {error, {already_started, Pid}} | %% The 'already_started' is returned only if Name is given %%----------------------------------------------------------------- -spec start(module(), linkage(), emgr_name(), module(), term(), [option()]) -> start_ret(). start(GenMod, LinkP, Name, Mod, Args, Options) -> case where(Name) of undefined -> do_spawn(GenMod, LinkP, Name, Mod, Args, Options); Pid -> {error, {already_started, Pid}} end. -spec start(module(), linkage(), module(), term(), [option()]) -> start_ret(). start(GenMod, LinkP, Mod, Args, Options) -> do_spawn(GenMod, LinkP, Mod, Args, Options). %%----------------------------------------------------------------- %% Spawn the process (and link) maybe at another node. %% If spawn without link, set parent to ourselves 'self'!!! %%----------------------------------------------------------------- do_spawn(GenMod, link, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start_link(?MODULE, init_it, [GenMod, self(), self(), Mod, Args, Options], Time, spawn_opts(Options)); do_spawn(GenMod, monitor, Mod, Args, Options) -> Time = timeout(Options), Ret = proc_lib:start_monitor(?MODULE, init_it, [GenMod, self(), self(), Mod, Args, Options], Time, spawn_opts(Options)), monitor_return(Ret); do_spawn(GenMod, _, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start(?MODULE, init_it, [GenMod, self(), 'self', Mod, Args, Options], Time, spawn_opts(Options)). do_spawn(GenMod, link, Name, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start_link(?MODULE, init_it, [GenMod, self(), self(), Name, Mod, Args, Options], Time, spawn_opts(Options)); do_spawn(GenMod, monitor, Name, Mod, Args, Options) -> Time = timeout(Options), Ret = proc_lib:start_monitor(?MODULE, init_it, [GenMod, self(), self(), Name, Mod, Args, Options], Time, spawn_opts(Options)), monitor_return(Ret); do_spawn(GenMod, _, Name, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start(?MODULE, init_it, [GenMod, self(), 'self', Name, Mod, Args, Options], Time, spawn_opts(Options)). %% %% Adjust monitor returns for OTP gen behaviours... %% %% If an OTP behaviour is introduced that 'init_ack's %% other results, this has code has to be moved out %% into all behaviours as well as adjusted... %% monitor_return({{ok, Pid}, Mon}) when is_pid(Pid), is_reference(Mon) -> %% Successful start_monitor()... {ok, {Pid, Mon}}; monitor_return({Error, Mon}) when is_reference(Mon) -> %% Failure; wait for spawned process to terminate %% and release resources, then return the error... receive {'DOWN', Mon, process, _Pid, _Reason} -> ok end, Error. %%----------------------------------------------------------------- %% Initiate the new process. %% Register the name using the Rfunc function %% Calls the Mod:init/Args function. %% Finally an acknowledge is sent to Parent and the main %% loop is entered. %%----------------------------------------------------------------- init_it(GenMod, Starter, Parent, Mod, Args, Options) -> init_it2(GenMod, Starter, Parent, self(), Mod, Args, Options). init_it(GenMod, Starter, Parent, Name, Mod, Args, Options) -> case register_name(Name) of true -> init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options); {false, Pid} -> proc_lib:init_fail( Starter, {error, {already_started, Pid}}, {exit, normal}) end. init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options) -> GenMod:init_it(Starter, Parent, Name, Mod, Args, Options). %%----------------------------------------------------------------- %% Makes a synchronous call to a generic process. %% Request is sent to the Pid, and the response must be %% {Tag, Reply}. %%----------------------------------------------------------------- %%% New call function which uses the new monitor BIF %%% call(ServerId, Label, Request) call(Process, Label, Request) -> call(Process, Label, Request, ?default_timeout). %% Optimize a common case. call(Process, Label, Request, Timeout) when is_pid(Process), Timeout =:= infinity orelse is_integer(Timeout) andalso Timeout >= 0 -> do_call(Process, Label, Request, Timeout); call(Process, Label, Request, Timeout) when Timeout =:= infinity; is_integer(Timeout), Timeout >= 0 -> Fun = fun(Pid) -> do_call(Pid, Label, Request, Timeout) end, do_for_proc(Process, Fun). -dialyzer({no_improper_lists, do_call/4}). do_call(Process, _Label, _Request, _Timeout) when Process =:= self() -> exit(calling_self); do_call(Process, Label, Request, infinity) when (is_pid(Process) andalso (node(Process) == node())) orelse (element(2, Process) == node() andalso is_atom(element(1, Process)) andalso (tuple_size(Process) =:= 2)) -> Mref = erlang:monitor(process, Process), %% Local without timeout; no need to use alias since we unconditionally %% will wait for either a reply or a down message which corresponds to %% the process being terminated (as opposed to 'noconnection')... Process ! {Label, {self(), Mref}, Request}, receive {Mref, Reply} -> erlang:demonitor(Mref, [flush]), {ok, Reply}; {'DOWN', Mref, _, _, Reason} -> exit(Reason) end; do_call(Process, Label, Request, Timeout) when is_atom(Process) =:= false -> Mref = erlang:monitor(process, Process, [{alias,demonitor}]), Tag = [alias | Mref], %% OTP-24: %% Using alias to prevent responses after 'noconnection' and timeouts. %% We however still may call nodes responding via process identifier, so %% we still use 'noconnect' on send in order to try to send on the %% monitored connection, and not trigger a new auto-connect. %% erlang:send(Process, {Label, {self(), Tag}, Request}, [noconnect]), receive {[alias | Mref], Reply} -> erlang:demonitor(Mref, [flush]), {ok, Reply}; {'DOWN', Mref, _, _, noconnection} -> Node = get_node(Process), exit({nodedown, Node}); {'DOWN', Mref, _, _, Reason} -> exit(Reason) after Timeout -> erlang:demonitor(Mref, [flush]), receive {[alias | Mref], Reply} -> {ok, Reply} after 0 -> exit(timeout) end end. get_node(Process) -> %% We trust the arguments to be correct, i.e %% Process is either a local or remote pid, %% or a {Name, Node} tuple (of atoms) and in this %% case this node (node()) _is_ distributed and Node =/= node(). case Process of {_S, N} when is_atom(N) -> N; _ when is_pid(Process) -> node(Process) end. -spec send_request(Name::server_ref(), Tag::term(), Request::term()) -> request_id(). send_request(Process, Tag, Request) when is_pid(Process) -> do_send_request(Process, Tag, Request); send_request(Process, Tag, Request) -> Fun = fun(Pid) -> do_send_request(Pid, Tag, Request) end, try do_for_proc(Process, Fun) catch exit:Reason -> %% Make send_request async and fake a down message Mref = erlang:make_ref(), self() ! {'DOWN', Mref, process, Process, Reason}, Mref end. -spec send_request(Name::server_ref(), Tag::term(), Request::term(), Label::term(), ReqIdCol::request_id_collection()) -> request_id_collection(). send_request(Process, Tag, Request, Label, ReqIdCol) when is_map(ReqIdCol) -> maps:put(send_request(Process, Tag, Request), Label, ReqIdCol). -dialyzer({no_improper_lists, do_send_request/3}). do_send_request(Process, Tag, Request) -> ReqId = erlang:monitor(process, Process, [{alias, demonitor}]), _ = erlang:send(Process, {Tag, {self(), [alias|ReqId]}, Request}, [noconnect]), ReqId. -spec '@wait_response_recv_opt'(term(), term(), term()) -> ok. '@wait_response_recv_opt'(Process, Tag, Request) -> %% Enables reference optimization in wait_response/2 and %% receive_response/2 %% %% This never actually runs and is only used to trigger the optimization, %% see the module comment in beam_ssa_recv for details. _ = wait_response(send_request(Process, Tag, Request), infinity), _ = receive_response(send_request(Process, Tag, Request), infinity), ok. %% %% Wait for a reply to the client. %% Note: if timeout is returned monitors are kept. -spec wait_response(ReqId, Timeout) -> Result when ReqId :: request_id(), Timeout :: response_timeout(), Resp :: {reply, Reply::term()} | {error, {Reason::term(), server_ref()}}, Result :: Resp | 'timeout'. wait_response(ReqId, Timeout) -> TMO = timeout_value(Timeout), receive {[alias|ReqId], Reply} -> erlang:demonitor(ReqId, [flush]), {reply, Reply}; {'DOWN', ReqId, _, Object, Reason} -> {error, {Reason, Object}} after TMO -> timeout end. -spec wait_response(ReqIdCol, Timeout, Delete) -> Result when ReqIdCol :: request_id_collection(), Timeout :: response_timeout(), Delete :: boolean(), Resp :: {reply, Reply::term()} | {error, {Reason::term(), server_ref()}}, Result :: {Resp, Label::term(), NewReqIdCol::request_id_collection()} | 'no_request' | 'timeout'. wait_response(ReqIdCol, Timeout, Delete) when map_size(ReqIdCol) == 0, is_boolean(Delete) -> _ = timeout_value(Timeout), no_request; wait_response(ReqIdCol, Timeout, Delete) when is_map(ReqIdCol), is_boolean(Delete) -> TMO = timeout_value(Timeout), receive {[alias|ReqId], _} = Msg when is_map_key(ReqId, ReqIdCol) -> collection_result(Msg, ReqIdCol, Delete); {'DOWN', ReqId, _, _, _} = Msg when is_map_key(ReqId, ReqIdCol) -> collection_result(Msg, ReqIdCol, Delete) after TMO -> timeout end. -spec receive_response(ReqId, Timeout) -> Result when ReqId :: request_id(), Timeout :: response_timeout(), Resp :: {reply, Reply::term()} | {error, {Reason::term(), server_ref()}}, Result :: Resp | 'timeout'. receive_response(ReqId, Timeout) -> TMO = timeout_value(Timeout), receive {[alias|ReqId], Reply} -> erlang:demonitor(ReqId, [flush]), {reply, Reply}; {'DOWN', ReqId, _, Object, Reason} -> {error, {Reason, Object}} after TMO -> erlang:demonitor(ReqId, [flush]), receive {[alias|ReqId], Reply} -> {reply, Reply} after 0 -> timeout end end. -spec receive_response(ReqIdCol, Timeout, Delete) -> Result when ReqIdCol :: request_id_collection(), Timeout :: response_timeout(), Delete :: boolean(), Resp :: {reply, Reply::term()} | {error, {Reason::term(), server_ref()}}, Result :: {Resp, Label::term(), NewReqIdCol::request_id_collection()} | 'no_request' | 'timeout'. receive_response(ReqIdCol, Timeout, Delete) when map_size(ReqIdCol) == 0, is_boolean(Delete) -> _ = timeout_value(Timeout), no_request; receive_response(ReqIdCol, Timeout, Delete) when is_map(ReqIdCol), is_boolean(Delete) -> TMO = timeout_value(Timeout), receive {[alias|ReqId], _} = Msg when is_map_key(ReqId, ReqIdCol) -> collection_result(Msg, ReqIdCol, Delete); {'DOWN', Mref, _, _, _} = Msg when is_map_key(Mref, ReqIdCol) -> collection_result(Msg, ReqIdCol, Delete) after TMO -> maps:foreach(fun (ReqId, _Label) when is_reference(ReqId) -> erlang:demonitor(ReqId, [flush]); (_, _) -> error(badarg) end, ReqIdCol), flush_responses(ReqIdCol), timeout end. -spec check_response(Msg::term(), ReqIdOrReqIdCol) -> Result when ReqIdOrReqIdCol :: request_id() | request_id_collection(), ReqIdResp :: {reply, Reply::term()} | {error, {Reason::term(), server_ref()}}, ReqIdColResp :: {{reply, Reply::term()}, Label::term()} | {{error, {Reason::term(), server_ref()}}, Label::term()}, Result :: ReqIdResp | ReqIdColResp | 'no_reply'. check_response(Msg, ReqId) when is_reference(ReqId) -> case Msg of {[alias|ReqId], Reply} -> erlang:demonitor(ReqId, [flush]), {reply, Reply}; {'DOWN', ReqId, _, Object, Reason} -> {error, {Reason, Object}}; _ -> no_reply end; check_response(_, _) -> error(badarg). -spec check_response(Msg, ReqIdCol, Delete) -> Result when Msg :: term(), ReqIdCol :: request_id_collection(), Delete :: boolean(), Resp :: {reply, Reply::term()} | {error, {Reason::term(), server_ref()}}, Result :: {Resp, Label::term(), NewReqIdCol::request_id_collection()} | 'no_request' | 'no_reply'. check_response(_Msg, ReqIdCol, Delete) when map_size(ReqIdCol) == 0, is_boolean(Delete) -> no_request; check_response(Msg, ReqIdCol, Delete) when is_map(ReqIdCol), is_boolean(Delete) -> case Msg of {[alias|ReqId], _} = Msg when is_map_key(ReqId, ReqIdCol) -> collection_result(Msg, ReqIdCol, Delete); {'DOWN', Mref, _, _, _} = Msg when is_map_key(Mref, ReqIdCol) -> collection_result(Msg, ReqIdCol, Delete); _ -> no_reply end. collection_result({[alias|ReqId], Reply}, ReqIdCol, Delete) -> _ = erlang:demonitor(ReqId, [flush]), collection_result({reply, Reply}, ReqId, ReqIdCol, Delete); collection_result({'DOWN', ReqId, _, Object, Reason}, ReqIdCol, Delete) -> collection_result({error, {Reason, Object}}, ReqId, ReqIdCol, Delete). collection_result(Resp, ReqId, ReqIdCol, false) -> {Resp, maps:get(ReqId, ReqIdCol), ReqIdCol}; collection_result(Resp, ReqId, ReqIdCol, true) -> {Label, NewReqIdCol} = maps:take(ReqId, ReqIdCol), {Resp, Label, NewReqIdCol}. flush_responses(ReqIdCol) -> receive {[alias|Mref], _Reply} when is_map_key(Mref, ReqIdCol) -> flush_responses(ReqIdCol) after 0 -> ok end. timeout_value(infinity) -> infinity; timeout_value(Timeout) when 0 =< Timeout, Timeout =< ?MAX_INT_TIMEOUT -> Timeout; timeout_value({abs, Timeout}) when is_integer(Timeout) -> case Timeout - erlang:monotonic_time(millisecond) of TMO when TMO < 0 -> 0; TMO when TMO > ?MAX_INT_TIMEOUT -> error(badarg); TMO -> TMO end; timeout_value(_) -> error(badarg). -spec reqids_new() -> NewReqIdCol::request_id_collection(). reqids_new() -> maps:new(). -spec reqids_size(request_id_collection()) -> non_neg_integer(). reqids_size(ReqIdCol) when is_map(ReqIdCol) -> maps:size(ReqIdCol); reqids_size(_) -> error(badarg). -spec reqids_add(ReqId::request_id(), Label::term(), ReqIdCol::request_id_collection()) -> NewReqIdCol::request_id_collection(). reqids_add(ReqId, _, ReqIdCol) when is_reference(ReqId), is_map_key(ReqId, ReqIdCol) -> error(badarg); reqids_add(ReqId, Label, ReqIdCol) when is_reference(ReqId), is_map(ReqIdCol) -> maps:put(ReqId, Label, ReqIdCol); reqids_add(_, _, _) -> error(badarg). -spec reqids_to_list(ReqIdCol::request_id_collection()) -> [{ReqId::request_id(), Label::term()}]. reqids_to_list(ReqIdCol) when is_map(ReqIdCol) -> maps:to_list(ReqIdCol); reqids_to_list(_) -> error(badarg). %% %% Send a reply to the client. %% reply({_To, [alias|Alias] = Tag}, Reply) when is_reference(Alias) -> Alias ! {Tag, Reply}, ok; reply({_To, [[alias|Alias] | _] = Tag}, Reply) when is_reference(Alias) -> Alias ! {Tag, Reply}, ok; reply({To, Tag}, Reply) -> try To ! {Tag, Reply}, ok catch _:_ -> ok end. %%----------------------------------------------------------------- %% Syncronously stop a generic process %%----------------------------------------------------------------- stop(Process) -> stop(Process, normal, infinity). stop(Process, Reason, Timeout) when Timeout =:= infinity; is_integer(Timeout), Timeout >= 0 -> Fun = fun(Pid) -> proc_lib:stop(Pid, Reason, Timeout) end, do_for_proc(Process, Fun). %%----------------------------------------------------------------- %% Map different specifications of a process to either Pid or %% {Name,Node}. Execute the given Fun with the process as only %% argument. %% ----------------------------------------------------------------- %% Local or remote by pid do_for_proc(Pid, Fun) when is_pid(Pid) -> Fun(Pid); %% Local by name do_for_proc(Name, Fun) when is_atom(Name) -> case whereis(Name) of Pid when is_pid(Pid) -> Fun(Pid); undefined -> exit(noproc) end; %% Global by name do_for_proc(Process, Fun) when ((tuple_size(Process) == 2 andalso element(1, Process) == global) orelse (tuple_size(Process) == 3 andalso element(1, Process) == via)) -> case where(Process) of Pid when is_pid(Pid) -> Node = node(Pid), try Fun(Pid) catch exit:{nodedown, Node} -> %% A nodedown not yet detected by global, %% pretend that it was. exit(noproc) end; undefined -> exit(noproc) end; %% Local by name in disguise do_for_proc({Name, Node}, Fun) when Node =:= node() -> do_for_proc(Name, Fun); %% Remote by name do_for_proc({_Name, Node} = Process, Fun) when is_atom(Node) -> if node() =:= nonode@nohost -> exit({nodedown, Node}); true -> Fun(Process) end. %%%----------------------------------------------------------------- %%% Misc. functions. %%%----------------------------------------------------------------- where({global, Name}) -> global:whereis_name(Name); where({via, Module, Name}) -> Module:whereis_name(Name); where({local, Name}) -> whereis(Name). register_name({local, Name} = LN) -> try register(Name, self()) of true -> true catch error:_ -> {false, where(LN)} end; register_name({global, Name} = GN) -> case global:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end; register_name({via, Module, Name} = GN) -> case Module:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end. name({local,Name}) -> Name; name({global,Name}) -> Name; name({via,_, Name}) -> Name; name(Pid) when is_pid(Pid) -> Pid. unregister_name({local,Name}) -> try unregister(Name) of _ -> ok catch _:_ -> ok end; unregister_name({global,Name}) -> _ = global:unregister_name(Name), ok; unregister_name({via, Mod, Name}) -> _ = Mod:unregister_name(Name), ok; unregister_name(Pid) when is_pid(Pid) -> ok. get_proc_name(Pid) when is_pid(Pid) -> Pid; get_proc_name({local, Name}) -> case process_info(self(), registered_name) of {registered_name, Name} -> Name; {registered_name, _Name} -> exit(process_not_registered); [] -> exit(process_not_registered) end; get_proc_name({global, Name}) -> case global:whereis_name(Name) of undefined -> exit(process_not_registered_globally); Pid when Pid =:= self() -> Name; _Pid -> exit(process_not_registered_globally) end; get_proc_name({via, Mod, Name}) -> case Mod:whereis_name(Name) of undefined -> exit({process_not_registered_via, Mod}); Pid when Pid =:= self() -> Name; _Pid -> exit({process_not_registered_via, Mod}) end. get_parent() -> case get('$ancestors') of [Parent | _] when is_pid(Parent) -> Parent; [Parent | _] when is_atom(Parent) -> name_to_pid(Parent); _ -> exit(process_was_not_started_by_proc_lib) end. name_to_pid(Name) -> case whereis(Name) of undefined -> case global:whereis_name(Name) of undefined -> exit(could_not_find_registered_name); Pid -> Pid end; Pid -> Pid end. timeout(Options) -> case lists:keyfind(timeout, 1, Options) of {_,Time} -> Time; false -> infinity end. spawn_opts(Options) -> case lists:keyfind(spawn_opt, 1, Options) of {_,Opts} -> Opts; false -> [] end. hibernate_after(Options) -> case lists:keyfind(hibernate_after, 1, Options) of {_,HibernateAfterTimeout} -> HibernateAfterTimeout; false -> infinity end. debug_options(Name, Opts) -> case lists:keyfind(debug, 1, Opts) of {_,Options} -> try sys:debug_options(Options) catch _:_ -> error_logger:format( "~tp: ignoring erroneous debug options - ~tp~n", [Name,Options]), [] end; false -> [] end. format_status_header(TagLine, Pid) when is_pid(Pid) -> lists:concat([TagLine, " ", pid_to_list(Pid)]); format_status_header(TagLine, RegName) when is_atom(RegName) -> lists:concat([TagLine, " ", RegName]); format_status_header(TagLine, Name) -> {TagLine, Name}. -spec format_status(Mod :: module(), Opt :: terminate | normal, Status, Args) -> ReturnStatus when Status :: #{ atom() => term() }, ReturnStatus :: #{ atom() => term(), '$status' => term()}, Args :: list(term()) | undefined. format_status(Mod, Opt, Status, Args) -> case {erlang:function_exported(Mod, format_status, 1), erlang:function_exported(Mod, format_status, 2)} of {true, _} -> try Mod:format_status(Status) of NewStatus when is_map(NewStatus) -> MergedStatus = maps:merge(Status, NewStatus), case maps:size(MergedStatus) =:= maps:size(NewStatus) of true -> MergedStatus; false -> Status#{ 'EXIT' => atom_to_list(Mod) ++ ":format_status/1 returned a map with unknown keys" } end; _ -> Status#{ 'EXIT' => atom_to_list(Mod) ++ ":format_status/1 did not return a map" } catch _:_ -> Status#{ 'EXIT' => atom_to_list(Mod) ++ ":format_status/1 crashed" } end; {false, true} when is_list(Args) -> try Mod:format_status(Opt, Args) of Result -> Status#{ '$status' => Result } catch throw:Result -> Status#{ '$status' => Result }; _:_ -> Status#{ 'EXIT' => atom_to_list(Mod) ++ ":format_status/2 crashed" } end; {false, _} -> Status end.