%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1996-2020. 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(application_controller). %% External exports -export([start/1, load_application/1, unload_application/1, start_application/2, start_boot_application/2, stop_application/1, control_application/1, change_application_data/2, prep_config_change/0, config_change/1, which_applications/0, which_applications/1, loaded_applications/0, info/0, set_env/2, get_pid_env/2, get_env/2, get_pid_all_env/1, get_all_env/1, get_pid_key/2, get_key/2, get_pid_all_key/1, get_all_key/1, get_master/1, get_application/1, get_application_module/1, start_type/1, permit_application/2, do_config_diff/2, set_env/3, set_env/4, unset_env/2, unset_env/3]). %% Internal exports -export([handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, init_starter/4, get_loaded/1]). %% logger callback -export([format_log/1, format_log/2]). %% Test exports, only to be used from the test suites -export([test_change_apps/2]). -import(lists, [zf/2, map/2, foreach/2, foldl/3, keyfind/3, keydelete/3, keyreplace/4]). -include("application_master.hrl"). -include("logger.hrl"). -define(AC, ?MODULE). % Name of process %%%----------------------------------------------------------------- %%% The application_controller controls local applications only. A %%% local application can be loaded/started/stopped/unloaded and %%% changed. The control of distributed applications is taken care of %%% by another process (default is dist_ac). %%% %%% When an application has been started (by a call to application:start) %%% it can be running or not running (on this node). For example, %%% a distributed application must be started on all nodes, but %%% may be running on one node at the time. %%% %%% The external API to this module is in the module 'application'. %%% %%% The process that controls distributed applications (called dist %%% ac). calls application_controller:control_application(Name) to %%% take responsibility for an application. The interface between AC %%% and the dist_ac process is message-based: %%% %%% AC DIST AC %%% == ======= %%% --> {ac_load_application_req, Name} %%% <-- {ac_load_application_reply, Name, LoadReply} %%% --> {ac_start_application_req, Name} (*) %%% <-- {ac_start_application_reply, Name, StartReply} %%% --> {ac_application_run, Name, Res} %%% --> {ac_application_not_run, Name, Res} %%% --> {ac_application_stopped, Name} %%% --> {ac_application_unloaded, Name} %%% <-- {ac_change_application_req, Name, Req} (**) %%% %%% Where LoadReply = %%% ok - App is loaded %%% {error, R} - An error occurred %%% And StartReply = %%% start_it - DIST AC decided that AC should start the app %%% {started, Node} - The app is started distributed at Node %%% not_started - The app should not be running at this time %%% {takeover, Node}- The app should takeover from Node %%% {error, R} - an error occurred %%% And Req = %%% start_it - DIST AC wants AC to start the app locally %%% stop_it - AC should stop the app. %%% {takeover, Node, RestartType} %%% - AC should start the app as a takeover %%% {failover, Node, RestartType} %%% - AC should start the app as a failover %%% {started, Node} - The app is started at Node %%% NOTE: The app must have been started at this node %%% before this request is sent! %%% And Res = %%% ok - Application is started locally %%% {error, R} - Start of application failed %%% %%% (*) %%% The call to application:start() doesn't return until the %%% ac_start_application_reply has been received by AC. AC %%% itself is not blocked however. %%% (**) %%% DIST AC gets ACK to its ac_change_application_req, but not as a %%% separate messgage. Instead the normal messages are used as: %%% start_it generates an ac_application_run %%% stop_it generates an ac_application_not_run %%% takeover generates an ac_application_run %%% started doesn't generate anything %%% %%% There is a distinction between application:stop and stop_it %%% from a dist ac process. The first one stops the application, %%% and resets the internal structures as they were before start was %%% called. stop_it stops the application, but just marks it as %%% not being running. %%% %%% When a dist ac process has taken control of an application, no %%% other process can take the control. %%%----------------------------------------------------------------- %%----------------------------------------------------------------- %% Naming conventions: %% App = appl_descr() %% Appl = #appl %% AppName = atom() %% Application = App | AppName %%----------------------------------------------------------------- -type appname() :: atom(). -record(state, {loading = [], starting = [], start_p_false = [], running = [], control = [], started = [], start_req = [], conf_data}). -type state() :: #state{}. %%----------------------------------------------------------------- %% loading = [{AppName, From}] - Load not yet finished %% starting = [{AppName, RestartType, Type, From}] - Start not %% yet finished %% start_p_false = [{AppName, RestartType, Type, From}] - Start not %% executed because permit == false %% running = [{AppName, Pid}] - running locally (Pid == application_master) %% [{AppName, {distributed, Node}}] - running on Node %% control = [{AppName, Controller}] %% started = [{AppName, RestartType}] - Names of all apps that %% have been started (but may not run because %% permission = false) %% conf_data = [{AppName, Env}] %% start_req = [{AppName, From}] - list of all start requests %% Id = AMPid | undefined | {distributed, Node} %% Env = [{Key, Value}] %%----------------------------------------------------------------- -record(appl, {name, appl_data, descr, id, vsn, restart_type, inc_apps, apps}). %%----------------------------------------------------------------- %% Func: start/1 %% Args: KernelApp = appl_descr() %% appl_descr() = [{application, Name, [appl_opt()]}] %% appl_opt() = {description, string()} | %% {vsn, string()} | %% {id, string()}, | %% {modules, [Module]} | %% {registered, [atom()]} | %% {applications, [atom()]} | %% {included_applications, [atom()]} | %% {env, [{atom(), term()}]} | %% {start_phases, [{atom(), term()}]}| %% {maxT, integer()|infinity} | %% {maxP, integer()|infinity} | %% {mod, {Module, term()}} %% Module = atom() %% Purpose: Starts the application_controller. This process starts all %% application masters for the applications. %% The kernel application is the only application that is %% treated specially. The reason for this is that the kernel %% starts user. This process is special because it should %% be group_leader for this process. %% Pre: All modules are loaded, or will be loaded on demand. %% Returns: {ok, Pid} | ReasonStr %%----------------------------------------------------------------- start(KernelApp) -> %% OTP-5811 Don't start as a gen_server to prevent crash report %% when (if) the process terminates Init = self(), AC = spawn_link(fun() -> init(Init, KernelApp) end), receive {ack, AC, ok} -> {ok, AC}; {ack, AC, {error, Reason}} -> to_string(Reason); % init doesn't want error tuple, only a reason {'EXIT', _Pid, Reason} -> to_string(Reason) end. %%----------------------------------------------------------------- %% Func: load_application/1 %% Args: Application = appl_descr() | atom() %% Purpose: Loads an application. Currently just inserts the %% application's env. %% Returns: ok | {error, Reason} %%----------------------------------------------------------------- load_application(Application) -> gen_server:call(?AC, {load_application, Application}, infinity). unload_application(AppName) -> gen_server:call(?AC, {unload_application, AppName}, infinity). %%----------------------------------------------------------------- %% Func: start_application/2 %% Args: Application = atom() %% RestartType = permanent | transient | temporary %% Purpose: Starts a new application. %% The RestartType specifies what should happen if the %% application dies: %% If it is permanent, all other applications are terminated, %% and the application_controller dies. %% If it is transient, and the application dies normally, %% this is reported and no other applications are terminated. %% If the application dies abnormally, all other applications %% are terminated, and the application_controller dies. %% If it is temporary and the application dies this is reported %% and no other applications are terminated. In this way, %% an application can run in test mode, without disturbing %% the other applications. %% The caller of this function is suspended until the application %% is started, either locally or distributed. %% Returns: ok | {error, Reason} %%----------------------------------------------------------------- start_application(AppName, RestartType) -> gen_server:call(?AC, {start_application, AppName, RestartType}, infinity). %%----------------------------------------------------------------- %% Func: start_boot_application/2 %% The same as start_application/2 expect that this function is %% called from the boot script file. It mustnot be used by the operator. %% This function will cause a node crash if a permanent application %% fails to boot start %%----------------------------------------------------------------- start_boot_application(Application, RestartType) -> case {application:load(Application), RestartType} of {ok, _} -> AppName = get_appl_name(Application), gen_server:call(?AC, {start_application, AppName, RestartType}, infinity); {{error, {already_loaded, AppName}}, _} -> gen_server:call(?AC, {start_application, AppName, RestartType}, infinity); {{error,{bad_environment_value,Env}}, permanent} -> Txt = io_lib:format("Bad environment variable: ~tp Application: ~p", [Env, Application]), exit({error, list_to_atom(lists:flatten(Txt))}); {Error, _} -> Error end. stop_application(AppName) -> gen_server:call(?AC, {stop_application, AppName}, infinity). %%----------------------------------------------------------------- %% Returns: [{Name, Descr, Vsn}] %%----------------------------------------------------------------- which_applications() -> gen_server:call(?AC, which_applications). which_applications(Timeout) -> gen_server:call(?AC, which_applications, Timeout). loaded_applications() -> ets:select(ac_tab, [{ {{loaded, '$1'}, #appl{descr = '$2', vsn = '$3', _ = '_'}}, [], [{{'$1', '$2', '$3'}}] }]). %% Returns some debug info info() -> gen_server:call(?AC, info). control_application(AppName) -> gen_server:call(?AC, {control_application, AppName}, infinity). %%----------------------------------------------------------------- %% Func: change_application_data/2 %% Args: Applications = [appl_descr()] %% Config = [{AppName, [{Par,Val}]}] %% Purpose: Change all applications and their parameters on this node. %% This function should be used from a release handler, at %% the same time as the .app or start.boot file is %% introduced. Note that during some time the ACs may have %% different view of e.g. the distributed applications. %% This is solved by syncing the release installation. %% However, strange things may happen if a node crashes %% and two other nodes have different opinons about who's %% gonna start the applications. The release handler must %% shutdown each involved node in this case. %% Note that this function is used to change existing apps, %% adding new/deleting old isn't handled by this function. %% Changes an application's vsn, descr and env. %% Returns: ok | {error, Reason} %% If an error occurred, the situation may be inconsistent, %% so the release handler must restart the node. E.g. if %% some applicatation may have got new config data. %%----------------------------------------------------------------- change_application_data(Applications, Config) -> gen_server:call(?AC, {change_application_data, Applications, Config}, infinity). prep_config_change() -> gen_server:call(?AC, prep_config_change, infinity). config_change(EnvPrev) -> gen_server:call(?AC, {config_change, EnvPrev}, infinity). get_pid_env(Master, Key) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of [[AppName]] -> get_env(AppName, Key); _ -> undefined end. get_env(AppName, Key) -> case ets:lookup(ac_tab, {env, AppName, Key}) of [{_, Val}] -> {ok, Val}; _ -> undefined end. get_pid_all_env(Master) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of [[AppName]] -> get_all_env(AppName); _ -> [] end. get_all_env(AppName) -> map(fun([Key, Val]) -> {Key, Val} end, ets:match(ac_tab, {{env, AppName, '$1'}, '$2'})). get_pid_key(Master, Key) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of [[AppName]] -> get_key(AppName, Key); _ -> undefined end. get_key(AppName, Key) -> case ets:lookup(ac_tab, {loaded, AppName}) of [{_, Appl}] -> case Key of description -> {ok, Appl#appl.descr}; id -> {ok, Appl#appl.id}; vsn -> {ok, Appl#appl.vsn}; modules -> {ok, (Appl#appl.appl_data)#appl_data.mods}; maxP -> {ok, (Appl#appl.appl_data)#appl_data.maxP}; maxT -> {ok, (Appl#appl.appl_data)#appl_data.maxT}; registered -> {ok, (Appl#appl.appl_data)#appl_data.regs}; included_applications -> {ok, Appl#appl.inc_apps}; applications -> {ok, Appl#appl.apps}; env -> {ok, get_all_env(AppName)}; mod -> {ok, (Appl#appl.appl_data)#appl_data.mod}; start_phases -> {ok, (Appl#appl.appl_data)#appl_data.phases}; _ -> undefined end; _ -> undefined end. get_pid_all_key(Master) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of [[AppName]] -> get_all_key(AppName); _ -> [] end. get_all_key(AppName) -> case ets:lookup(ac_tab, {loaded, AppName}) of [{_, Appl}] -> {ok, [{description, Appl#appl.descr}, {id, Appl#appl.id}, {vsn, Appl#appl.vsn}, {modules, (Appl#appl.appl_data)#appl_data.mods}, {maxP, (Appl#appl.appl_data)#appl_data.maxP}, {maxT, (Appl#appl.appl_data)#appl_data.maxT}, {registered, (Appl#appl.appl_data)#appl_data.regs}, {included_applications, Appl#appl.inc_apps}, {applications, Appl#appl.apps}, {env, get_all_env(AppName)}, {mod, (Appl#appl.appl_data)#appl_data.mod}, {start_phases, (Appl#appl.appl_data)#appl_data.phases} ]}; _ -> undefined end. start_type(Master) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of [[AppName]] -> gen_server:call(?AC, {start_type, AppName}, infinity); _X -> undefined end. get_master(AppName) -> case ets:lookup(ac_tab, {application_master, AppName}) of [{_, Pid}] -> Pid; _ -> undefined end. get_application(Master) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of [[AppName]] -> {ok, AppName}; _ -> undefined end. get_application_module(Module) -> ApplDataPattern = #appl_data{mods='$2', _='_'}, ApplPattern = #appl{appl_data=ApplDataPattern, _='_'}, AppModules = ets:match(ac_tab, {{loaded, '$1'}, ApplPattern}), get_application_module(Module, AppModules). get_application_module(Module, [[AppName, Modules]|AppModules]) -> case lists:member(Module, Modules) of true -> {ok, AppName}; false -> get_application_module(Module, AppModules) end; get_application_module(_Module, []) -> undefined. permit_application(ApplName, Flag) -> gen_server:call(?AC, {permit_application, ApplName, Flag}, infinity). set_env(Config, Opts) -> case check_conf_data(Config) of ok -> Timeout = proplists:get_value(timeout, Opts, 5000), gen_server:call(?AC, {set_env, Config, Opts}, Timeout); {error, _} = Error -> Error end. set_env(AppName, Key, Val) -> gen_server:call(?AC, {set_env, AppName, Key, Val, []}). set_env(AppName, Key, Val, Opts) -> Timeout = proplists:get_value(timeout, Opts, 5000), gen_server:call(?AC, {set_env, AppName, Key, Val, Opts}, Timeout). unset_env(AppName, Key) -> gen_server:call(?AC, {unset_env, AppName, Key, []}). unset_env(AppName, Key, Opts) -> Timeout = proplists:get_value(timeout, Opts, 5000), gen_server:call(?AC, {unset_env, AppName, Key, Opts}, Timeout). %%%----------------------------------------------------------------- %%% call-back functions from gen_server %%%----------------------------------------------------------------- init(Init, Kernel) -> register(?AC, self()), process_flag(trap_exit, true), put('$ancestors', [Init]), % OTP-5811, for gen_server compatibility put('$initial_call', {application_controller, start, 1}), case catch check_conf() of {ok, ConfData} -> %% Actually, we don't need this info in an ets table anymore. %% This table was introduced because starting applications %% should be able to get som info from AC (e.g. loaded_apps). %% The new implementation makes sure the AC process can be %% called during start-up of any app. case check_conf_data(ConfData) of ok -> _ = ets:new(ac_tab, [set, public, named_table, {read_concurrency,true}]), S = #state{conf_data = ConfData}, {ok, KAppl} = make_appl(Kernel), case catch load(S, KAppl) of {'EXIT', LoadError} -> Reason = {'load error', LoadError}, Init ! {ack, self(), {error, to_string(Reason)}}; {error, Error} -> Init ! {ack, self(), {error, to_string(Error)}}; {ok, NewS} -> Init ! {ack, self(), ok}, gen_server:enter_loop(?MODULE, [], NewS, {local, ?AC}) end; {error, ErrorStr} -> Str = lists:flatten(io_lib:format("invalid config data: ~ts", [ErrorStr])), Init ! {ack, self(), {error, to_string(Str)}} end; {error, {File, Line, Str}} -> ReasonStr = lists:flatten(io_lib:format("error in config file " "~tp (~w): ~ts", [File, Line, Str])), Init ! {ack, self(), {error, to_string(ReasonStr)}} end. %% Check the syntax of the .config file %% [{ApplicationName, [{Parameter, Value}]}]. check_conf_data([]) -> ok; check_conf_data(ConfData) when is_list(ConfData) -> [Application | ConfDataRem] = ConfData, case Application of {AppName, List} when is_atom(AppName), is_list(List) -> case lists:keymember(AppName, 1, ConfDataRem) of true -> {error, "duplicate application config: " ++ atom_to_list(AppName)}; false -> case check_para(List, AppName) of ok -> check_conf_data(ConfDataRem); Error -> Error end end; {AppName, List} when is_list(List) -> ErrMsg = "application: " ++ lists:flatten(io_lib:format("~tp",[AppName])) ++ "; application name must be an atom", {error, ErrMsg}; {AppName, _List} -> ErrMsg = "application: " ++ lists:flatten(io_lib:format("~tp",[AppName])) ++ "; parameters must be a list", {error, ErrMsg}; Else -> ErrMsg = "invalid application config: " ++ lists:flatten(io_lib:format("~tp",[Else])), {error, ErrMsg} end; check_conf_data(_ConfData) -> {error, "configuration must be a list ended by "}. check_para([], _AppName) -> ok; check_para([{Para, Val} | ParaList], AppName) when is_atom(Para) -> case lists:keymember(Para, 1, ParaList) of true -> ErrMsg = "application: " ++ atom_to_list(AppName) ++ "; duplicate parameter: " ++ atom_to_list(Para), {error, ErrMsg}; false -> case check_para_value(Para, Val, AppName) of ok -> check_para(ParaList, AppName); {error, _} = Error -> Error end end; check_para([{Para, _Val} | _ParaList], AppName) -> {error, "application: " ++ atom_to_list(AppName) ++ "; invalid parameter name: " ++ lists:flatten(io_lib:format("~tp",[Para]))}; check_para([Else | _ParaList], AppName) -> {error, "application: " ++ atom_to_list(AppName) ++ "; invalid parameter: " ++ lists:flatten(io_lib:format("~tp",[Else]))}. check_para_value(distributed, Apps, kernel) -> check_distributed(Apps); check_para_value(_Para, _Val, _AppName) -> ok. %% Special check of distributed parameter for kernel check_distributed([]) -> ok; check_distributed([{App, List} | Apps]) when is_atom(App), is_list(List) -> check_distributed(Apps); check_distributed([{App, infinity, List} | Apps]) when is_atom(App), is_list(List) -> check_distributed(Apps); check_distributed([{App, Time, List} | Apps]) when is_atom(App), is_integer(Time), is_list(List) -> check_distributed(Apps); check_distributed(_Else) -> {error, "application: kernel; erroneous parameter: distributed"}. -type calls() :: 'info' | 'prep_config_change' | 'which_applications' | {'config_change' | 'control_application' | 'load_application' | 'start_type' | 'stop_application' | 'unload_application', term()} | {'change_application_data', _, _} | {'permit_application', atom() | {'application',atom(),_},_} | {'start_application', _, _} | {'unset_env', _, _, _} | {'set_env', _, _, _, _}. -spec handle_call(calls(), {pid(), term()}, state()) -> {'noreply', state()} | {'reply', term(), state()}. handle_call({load_application, Application}, From, S) -> case catch do_load_application(Application, S) of {ok, NewS} -> AppName = get_appl_name(Application), case cntrl(AppName, S, {ac_load_application_req, AppName}) of true -> {noreply, S#state{loading = [{AppName, From} | S#state.loading]}}; false -> {reply, ok, NewS} end; {error, _} = Error -> {reply, Error, S}; {'EXIT', R} -> {reply, {error, R}, S} end; handle_call({unload_application, AppName}, _From, S) -> case lists:keymember(AppName, 1, S#state.running) of true -> {reply, {error, {running, AppName}}, S}; false -> case get_loaded(AppName) of {true, _} -> NewS = unload(AppName, S), cntrl(AppName, S, {ac_application_unloaded, AppName}), {reply, ok, NewS}; false -> {reply, {error, {not_loaded, AppName}}, S} end end; handle_call({start_application, AppName, RestartType}, From, S) -> #state{running = Running, starting = Starting, start_p_false = SPF, started = Started, start_req = Start_req} = S, %% Check if the commandline environment variables are OK. %% Incase of erroneous variables do not start the application, %% if the application is permanent crash the node. %% Check if the application is already starting. case lists:keyfind(AppName, 1, Start_req) of false -> case catch check_start_cond(AppName, RestartType, Started, Running) of {ok, Appl} -> Cntrl = cntrl(AppName, S, {ac_start_application_req, AppName}), Perm = application:get_env(kernel, permissions), case {Cntrl, Perm} of {true, _} -> {noreply, S#state{starting = [{AppName, RestartType, normal, From} | Starting], start_req = [{AppName, From} | Start_req]}}; {false, undefined} -> spawn_starter(From, Appl, S, normal), {noreply, S#state{starting = [{AppName, RestartType, normal, From} | Starting], start_req = [{AppName, From} | Start_req]}}; {false, {ok, Perms}} -> case lists:member({AppName, false}, Perms) of false -> spawn_starter(From, Appl, S, normal), {noreply, S#state{starting = [{AppName, RestartType, normal, From} | Starting], start_req = [{AppName, From} | Start_req]}}; true -> SS = S#state{start_p_false = [{AppName, RestartType, normal, From} | SPF]}, {reply, ok, SS} end end; {error, _R} = Error -> {reply, Error, S} end; {AppName, _FromX} -> SS = S#state{start_req = [{AppName, From} | Start_req]}, {noreply, SS} end; handle_call({permit_application, AppName, Bool}, From, S) -> Control = S#state.control, Starting = S#state.starting, SPF = S#state.start_p_false, Started = S#state.started, Running = S#state.running, Start_req = S#state.start_req, IsLoaded = get_loaded(AppName), IsStarting = lists:keysearch(AppName, 1, Starting), IsSPF = lists:keysearch(AppName, 1, SPF), IsStarted = lists:keysearch(AppName, 1, Started), IsRunning = lists:keysearch(AppName, 1, Running), case lists:keymember(AppName, 1, Control) of %%======================== %% distributed application %%======================== true -> case {IsLoaded, IsStarting, IsStarted} of %% not loaded {false, _, _} -> {reply, {error, {not_loaded, AppName}}, S}; %% only loaded {{true, _Appl}, false, false} -> update_permissions(AppName, Bool), {reply, {distributed_application, only_loaded}, S}; _ -> update_permissions(AppName, Bool), {reply, distributed_application, S} end; %%======================== %% local application %%======================== false -> case {Bool, IsLoaded, IsStarting, IsSPF, IsStarted, IsRunning} of %%------------------------ %% permit the applicaition %%------------------------ %% already running {true, _, _, _, _, {value, _Tuple}} -> {reply, ok, S}; %% not loaded {true, false, _, _, _, _} -> {reply, {error, {not_loaded, AppName}}, S}; %% only loaded {true, {true, _Appl}, false, false, false, false} -> update_permissions(AppName, Bool), {reply, ok, S}; %% starting {true, {true, _Appl}, {value, _Tuple}, false, false, false} -> update_permissions(AppName, Bool), {reply, ok, S}; %% check the permission after then app is started %% start requested but not started because permit was false {true, {true, Appl}, false, {value, Tuple}, false, false} -> update_permissions(AppName, Bool), {_AppName2, RestartType, normal, _From} = Tuple, spawn_starter(From, Appl, S, normal), SS = S#state{starting = [{AppName, RestartType, normal, From} | Starting], start_p_false = keydelete(AppName, 1, SPF), start_req = [{AppName, From} | Start_req]}, {noreply, SS}; %% started but not running {true, {true, Appl}, _, _, {value, {AppName, RestartType}}, false} -> update_permissions(AppName, Bool), spawn_starter(From, Appl, S, normal), SS = S#state{starting = [{AppName, RestartType, normal, From} | Starting], started = keydelete(AppName, 1, Started), start_req = [{AppName, From} | Start_req]}, {noreply, SS}; %%========================== %% unpermit the application %%========================== %% running {false, _, _, _, _, {value, {_AppName, Id}}} -> {_AppName2, Type} = lists:keyfind(AppName, 1, Started), stop_appl(AppName, Id, Type), NRunning = keydelete(AppName, 1, Running), {reply, ok, S#state{running = NRunning}}; %% not loaded {false, false, _, _, _, _} -> {reply, {error, {not_loaded, AppName}}, S}; %% only loaded {false, {true, _Appl}, false, false, false, false} -> update_permissions(AppName, Bool), {reply, ok, S}; %% starting {false, {true, _Appl}, {value, _Tuple}, false, false, false} -> update_permissions(AppName, Bool), {reply, ok, S}; %% start requested but not started because permit was false {false, {true, _Appl}, false, {value, _Tuple}, false, false} -> update_permissions(AppName, Bool), SS = S#state{start_p_false = keydelete(AppName, 1, SPF)}, {reply, ok, SS}; %% started but not running {false, {true, _Appl}, _, _, {value, _Tuple}, false} -> update_permissions(AppName, Bool), {reply, ok, S} end end; handle_call({stop_application, AppName}, _From, S) -> #state{running = Running, started = Started} = S, case lists:keyfind(AppName, 1, Running) of {_AppName, Id} -> {_AppName2, Type} = lists:keyfind(AppName, 1, Started), stop_appl(AppName, Id, Type), NRunning = keydelete(AppName, 1, Running), NStarted = keydelete(AppName, 1, Started), cntrl(AppName, S, {ac_application_stopped, AppName}), {reply, ok, S#state{running = NRunning, started = NStarted}}; false -> case lists:keymember(AppName, 1, Started) of true -> NStarted = keydelete(AppName, 1, Started), cntrl(AppName, S, {ac_application_stopped, AppName}), {reply, ok, S#state{started = NStarted}}; false -> {reply, {error, {not_started, AppName}}, S} end end; handle_call({change_application_data, Applications, Config}, _From, S) -> OldAppls = ets:filter(ac_tab, fun([{{loaded, _AppName}, Appl}]) -> {true, Appl}; (_) -> false end, []), case catch do_change_apps(Applications, Config, OldAppls) of {error, _} = Error -> {reply, Error, S}; {'EXIT', R} -> {reply, {error, R}, S}; {NewAppls, NewConfig} -> lists:foreach(fun(Appl) -> ets:insert(ac_tab, {{loaded, Appl#appl.name}, Appl}) end, NewAppls), {reply, ok, S#state{conf_data = NewConfig}} end; handle_call(prep_config_change, _From, S) -> RunningApps = S#state.running, EnvBefore = lists:reverse(do_prep_config_change(RunningApps)), {reply, EnvBefore, S}; handle_call({config_change, EnvBefore}, _From, S) -> RunningApps = S#state.running, R = do_config_change(RunningApps, EnvBefore), {reply, R, S}; handle_call(which_applications, _From, S) -> Reply = zf(fun({Name, Id}) -> case Id of {distributed, _Node} -> false; _ -> {true, #appl{descr = Descr, vsn = Vsn}} = get_loaded(Name), {true, {Name, Descr, Vsn}} end end, S#state.running), {reply, Reply, S}; handle_call({set_env, Config, Opts}, _From, S) -> _ = [add_env(AppName, Env) || {AppName, Env} <- Config], case proplists:get_value(persistent, Opts, false) of true -> {reply, ok, S#state{conf_data = merge_env(S#state.conf_data, Config)}}; false -> {reply, ok, S} end; handle_call({set_env, AppName, Key, Val, Opts}, _From, S) -> ets:insert(ac_tab, {{env, AppName, Key}, Val}), case proplists:get_value(persistent, Opts, false) of true -> Fun = fun(Env) -> lists:keystore(Key, 1, Env, {Key, Val}) end, {reply, ok, S#state{conf_data = change_app_env(S#state.conf_data, AppName, Fun)}}; false -> {reply, ok, S} end; handle_call({unset_env, AppName, Key, Opts}, _From, S) -> ets:delete(ac_tab, {env, AppName, Key}), case proplists:get_value(persistent, Opts, false) of true -> Fun = fun(Env) -> lists:keydelete(Key, 1, Env) end, {reply, ok, S#state{conf_data = change_app_env(S#state.conf_data, AppName, Fun)}}; false -> {reply, ok, S} end; handle_call({control_application, AppName}, {Pid, _Tag}, S) -> Control = S#state.control, case lists:keymember(AppName, 1, Control) of false -> link(Pid), {reply, true, S#state{control = [{AppName, Pid} | Control]}}; true -> {reply, false, S} end; handle_call({start_type, AppName}, _From, S) -> Starting = S#state.starting, StartType = case lists:keyfind(AppName, 1, Starting) of false -> local; {_AppName, _RestartType, Type, _F} -> Type end, {reply, StartType, S}; handle_call(info, _From, S) -> Reply = [{loaded, loaded_applications()}, {loading, S#state.loading}, {started, S#state.started}, {start_p_false, S#state.start_p_false}, {running, S#state.running}, {starting, S#state.starting}], {reply, Reply, S}. -spec handle_cast({'application_started', appname(), _}, state()) -> {'noreply', state()} | {'stop', string(), state()}. handle_cast({application_started, AppName, Res}, S) -> handle_application_started(AppName, Res, S). handle_application_started(AppName, Res, S) -> #state{starting = Starting, running = Running, started = Started, start_req = Start_req} = S, Start_reqN = reply_to_requester(AppName, Start_req, Res), {_AppName, RestartType, _Type, _From} = lists:keyfind(AppName, 1, Starting), case Res of {ok, Id} -> case AppName of kernel -> check_user(); _ -> ok end, info_started(AppName, nd(Id)), notify_cntrl_started(AppName, Id, S, ok), NRunning = keyreplaceadd(AppName, 1, Running,{AppName,Id}), NStarted = keyreplaceadd(AppName, 1, Started,{AppName,RestartType}), NewS = S#state{starting = keydelete(AppName, 1, Starting), running = NRunning, started = NStarted, start_req = Start_reqN}, %% The permission may have been changed during start Perm = application:get_env(kernel, permissions), case {Perm, Id} of {undefined, _} -> {noreply, NewS}; %% Check only if the application is started on the own node {{ok, Perms}, {distributed, StartNode}} when StartNode =:= node() -> case lists:member({AppName, false}, Perms) of true -> #state{running = StopRunning, started = StopStarted} = NewS, case lists:keyfind(AppName, 1, StopRunning) of {_AppName, Id} -> {_AppName2, Type} = lists:keyfind(AppName, 1, StopStarted), stop_appl(AppName, Id, Type), NStopRunning = keydelete(AppName, 1, StopRunning), cntrl(AppName, NewS, {ac_application_stopped, AppName}), {noreply, NewS#state{running = NStopRunning, started = StopStarted}}; false -> {noreply, NewS} end; false -> {noreply, NewS} end; _ -> {noreply, NewS} end; {error, R} = Error when RestartType =:= temporary -> notify_cntrl_started(AppName, undefined, S, Error), info_exited(AppName, R, RestartType), {noreply, S#state{starting = keydelete(AppName, 1, Starting), start_req = Start_reqN}}; {info, R} when RestartType =:= temporary -> notify_cntrl_started(AppName, undefined, S, {error, R}), {noreply, S#state{starting = keydelete(AppName, 1, Starting), start_req = Start_reqN}}; {ErrInf, R} when RestartType =:= transient, ErrInf =:= error; RestartType =:= transient, ErrInf =:= info -> notify_cntrl_started(AppName, undefined, S, {error, R}), case ErrInf of error -> info_exited(AppName, R, RestartType); info -> ok end, case R of {{'EXIT',normal},_Call} -> {noreply, S#state{starting = keydelete(AppName, 1, Starting), start_req = Start_reqN}}; _ -> Reason = {application_start_failure, AppName, R}, {stop, to_string(Reason), S} end; {error, R} = Error -> %% permanent notify_cntrl_started(AppName, undefined, S, Error), info_exited(AppName, R, RestartType), Reason = {application_start_failure, AppName, R}, {stop, to_string(Reason), S}; {info, R} -> %% permanent notify_cntrl_started(AppName, undefined, S, {error, R}), Reason = {application_start_failure, AppName, R}, {stop, to_string(Reason), S} end. -spec handle_info(term(), state()) -> {'noreply', state()} | {'stop', string(), state()}. handle_info({ac_load_application_reply, AppName, Res}, S) -> case keysearchdelete(AppName, 1, S#state.loading) of {value, {_AppName, From}, Loading} -> gen_server:reply(From, Res), case Res of ok -> {noreply, S#state{loading = Loading}}; {error, _R} -> NewS = unload(AppName, S), {noreply, NewS#state{loading = Loading}} end; false -> {noreply, S} end; handle_info({ac_start_application_reply, AppName, Res}, S) -> Start_req = S#state.start_req, case lists:keyfind(AppName, 1, Starting = S#state.starting) of {_AppName, RestartType, Type, From} -> case Res of start_it -> {true, Appl} = get_loaded(AppName), spawn_starter(From, Appl, S, Type), {noreply, S}; {started, Node} -> handle_application_started(AppName, {ok, {distributed, Node}}, S); not_started -> Started = S#state.started, Start_reqN = reply_to_requester(AppName, Start_req, ok), {noreply, S#state{starting = keydelete(AppName, 1, Starting), started = [{AppName, RestartType} | Started], start_req = Start_reqN}}; {takeover, _Node} = Takeover -> {true, Appl} = get_loaded(AppName), spawn_starter(From, Appl, S, Takeover), NewStarting1 = keydelete(AppName, 1, Starting), NewStarting = [{AppName, RestartType, Takeover, From} | NewStarting1], {noreply, S#state{starting = NewStarting}}; {error, Reason} = Error when RestartType =:= permanent -> Start_reqN = reply_to_requester(AppName, Start_req, Error), {stop, to_string(Reason), S#state{start_req = Start_reqN}}; {error, _Reason} = Error -> Start_reqN = reply_to_requester(AppName, Start_req, Error), {noreply, S#state{starting = keydelete(AppName, 1, Starting), start_req = Start_reqN}} end; false -> {noreply, S} % someone called stop before control got that end; handle_info({ac_change_application_req, AppName, Msg}, S) -> Running = S#state.running, Started = S#state.started, Starting = S#state.starting, case {keyfind(AppName, 1, Running), keyfind(AppName, 1, Started)} of {{AppName, Id}, {_AppName2, Type}} -> case Msg of {started, Node} -> stop_appl(AppName, Id, Type), NRunning = [{AppName, {distributed, Node}} | keydelete(AppName, 1, Running)], {noreply, S#state{running = NRunning}}; {takeover, _Node, _RT} when is_pid(Id) -> % it is running already notify_cntrl_started(AppName, Id, S, ok), {noreply, S}; {takeover, Node, RT} -> NewS = do_start(AppName, RT, {takeover, Node}, undefined, S), {noreply, NewS}; {failover, _Node, _RT} when is_pid(Id) -> % it is running already notify_cntrl_started(AppName, Id, S, ok), {noreply, S}; {failover, Node, RT} -> case application:get_key(AppName, start_phases) of {ok, undefined} -> %% to be backwards compatible the application %% is not started as failover if start_phases %% is not defined in the .app file NewS = do_start(AppName, RT, normal, undefined, S), {noreply, NewS}; {ok, _StartPhases} -> NewS = do_start(AppName, RT, {failover, Node}, undefined, S), {noreply, NewS} end; stop_it -> stop_appl(AppName, Id, Type), cntrl(AppName, S, {ac_application_not_run, AppName}), NRunning = keyreplace(AppName, 1, Running, {AppName, {distributed, []}}), {noreply, S#state{running = NRunning}}; %% We should not try to start a running application! start_it when is_pid(Id) -> notify_cntrl_started(AppName, Id, S, ok), {noreply, S}; start_it -> NewS = do_start(AppName, undefined, normal, undefined, S), {noreply, NewS}; not_running -> NRunning = keydelete(AppName, 1, Running), {noreply, S#state{running = NRunning}}; _ -> {noreply, S} end; _ -> IsLoaded = get_loaded(AppName), IsStarting = lists:keysearch(AppName, 1, Starting), IsStarted = lists:keysearch(AppName, 1, Started), IsRunning = lists:keysearch(AppName, 1, Running), case Msg of start_it -> case {IsLoaded, IsStarting, IsStarted, IsRunning} of %% already running {_, _, _, {value, _Tuple}} -> {noreply, S}; %% not loaded {false, _, _, _} -> {noreply, S}; %% only loaded {{true, _Appl}, false, false, false} -> {noreply, S}; %% starting {{true, _Appl}, {value, Tuple}, false, false} -> {_AppName, _RStype, _Type, From} = Tuple, NewS = do_start(AppName, undefined, normal, From, S), {noreply, NewS}; %% started but not running {{true, _Appl}, _, {value, {AppName, _RestartType}}, false} -> NewS = do_start(AppName, undefined, normal, undefined, S), SS = NewS#state{started = keydelete(AppName, 1, Started)}, {noreply, SS} end; {started, Node} -> NRunning = [{AppName, {distributed, Node}} | keydelete(AppName, 1, Running)], {noreply, S#state{running = NRunning}}; _ -> {noreply, S} % someone called stop before control got that end end; %%----------------------------------------------------------------- %% An application died. Check its restart_type. Maybe terminate %% all other applications. %%----------------------------------------------------------------- handle_info({'EXIT', Pid, Reason}, S) -> ets:match_delete(ac_tab, {{application_master, '_'}, Pid}), NRunning = keydelete(Pid, 2, S#state.running), NewS = S#state{running = NRunning}, case lists:keyfind(Pid, 2, S#state.running) of {AppName, _AmPid} -> cntrl(AppName, S, {ac_application_stopped, AppName}), case lists:keyfind(AppName, 1, S#state.started) of {_AppName, temporary} -> info_exited(AppName, Reason, temporary), {noreply, NewS}; {_AppName, transient} when Reason =:= normal -> info_exited(AppName, Reason, transient), {noreply, NewS}; {_AppName, Type} -> info_exited(AppName, Reason, Type), {stop, to_string({application_terminated, AppName, Reason}), NewS} end; false -> {noreply, S#state{control = del_cntrl(S#state.control, Pid)}} end; handle_info(_, S) -> {noreply, S}. -spec terminate(term(), state()) -> 'ok'. terminate(Reason, S) -> case application:get_env(kernel, shutdown_func) of {ok, {M, F}} -> catch M:F(Reason); _ -> ok end, ShutdownTimeout = case application:get_env(kernel, shutdown_timeout) of undefined -> infinity; {ok,T} -> T end, foreach(fun({_AppName, Id}) when is_pid(Id) -> Ref = erlang:monitor(process, Id), unlink(Id), exit(Id, shutdown), receive %% Proc died before link {'EXIT', Id, _} -> ok after 0 -> receive {'DOWN', Ref, process, Id, _} -> ok after ShutdownTimeout -> exit(Id, kill), receive {'DOWN', Ref, process, Id, _} -> ok end end end; (_) -> ok end, S#state.running), true = ets:delete(ac_tab), ok. -spec code_change(term(), state(), term()) -> {'ok', state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%----------------------------------------------------------------- %%% Internal functions %%%----------------------------------------------------------------- cntrl(AppName, #state{control = Control}, Msg) -> case lists:keyfind(AppName, 1, Control) of {_AppName, Pid} -> Pid ! Msg, true; false -> false end. notify_cntrl_started(_AppName, {distributed, _Node}, _S, _Res) -> ok; notify_cntrl_started(AppName, _Id, S, Res) -> cntrl(AppName, S, {ac_application_run, AppName, Res}). del_cntrl([{_, Pid}|T], Pid) -> del_cntrl(T, Pid); del_cntrl([H|T], Pid) -> [H|del_cntrl(T, Pid)]; del_cntrl([], _Pid) -> []. get_loaded(App) -> AppName = get_appl_name(App), case ets:lookup(ac_tab, {loaded, AppName}) of [{_Key, Appl}] -> {true, Appl}; _ -> false end. do_load_application(Application, S) -> case get_loaded(Application) of {true, _} -> throw({error, {already_loaded, Application}}); false -> case make_appl(Application) of {ok, Appl} -> load(S, Appl); Error -> Error end end. %% Recursively load the application and its included apps. %load(S, {ApplData, ApplEnv, IncApps, Descr, Vsn, Apps}) -> load(S, {ApplData, ApplEnv, IncApps, Descr, Id, Vsn, Apps}) -> Name = ApplData#appl_data.name, ConfEnv = get_env_i(Name, S), NewEnv = merge_app_env(ApplEnv, ConfEnv), CmdLineEnv = get_cmd_env(Name), NewEnv2 = merge_app_env(NewEnv, CmdLineEnv), add_env(Name, NewEnv2), Appl = #appl{name = Name, descr = Descr, id = Id, vsn = Vsn, appl_data = ApplData, inc_apps = IncApps, apps = Apps}, ets:insert(ac_tab, {{loaded, Name}, Appl}), NewS = foldl(fun(App, S1) -> case get_loaded(App) of {true, _} -> S1; false -> case do_load_application(App, S1) of {ok, S2} -> S2; Error -> throw(Error) end end end, S, IncApps), {ok, NewS}. unload(AppName, S) -> {ok, IncApps} = get_key(AppName, included_applications), del_env(AppName), ets:delete(ac_tab, {loaded, AppName}), foldl(fun(App, S1) -> case get_loaded(App) of false -> S1; {true, _} -> unload(App, S1) end end, S, IncApps). check_start_cond(AppName, RestartType, Started, Running) -> validRestartType(RestartType), case get_loaded(AppName) of {true, Appl} -> %% Check Running; not Started. An exited app is not running, %% but started. It must be possible to start an exited app! case lists:keymember(AppName, 1, Running) of true -> {error, {already_started, AppName}}; false -> foreach( fun(AppName2) -> case lists:keymember(AppName2, 1, Started) of true -> ok; false -> throw({error, {not_started, AppName2}}) end end, Appl#appl.apps), {ok, Appl} end; false -> {error, {not_loaded, AppName}} end. do_start(AppName, RT, Type, From, S) -> RestartType = case lists:keyfind(AppName, 1, S#state.started) of {_AppName2, OldRT} -> get_restart_type(RT, OldRT); false -> RT end, %% UW 990913: We check start_req instead of starting, because starting %% has already been checked. case lists:keymember(AppName, 1, S#state.start_req) of false -> {true, Appl} = get_loaded(AppName), Start_req = S#state.start_req, spawn_starter(undefined, Appl, S, Type), Starting = case lists:keymember(AppName, 1, S#state.starting) of false -> %% UW: don't know if this is necessary [{AppName, RestartType, Type, From} | S#state.starting]; true -> S#state.starting end, S#state{starting = Starting, start_req = [{AppName, From} | Start_req]}; true -> % otherwise we're already starting the app... S end. spawn_starter(From, Appl, S, Type) -> spawn_link(?MODULE, init_starter, [From, Appl, S, Type]). init_starter(_From, Appl, S, Type) -> process_flag(trap_exit, true), AppName = Appl#appl.name, gen_server:cast(?AC, {application_started, AppName, catch start_appl(Appl, S, Type)}). reply(undefined, _Reply) -> ok; reply(From, Reply) -> gen_server:reply(From, Reply). start_appl(Appl, S, Type) -> ApplData = Appl#appl.appl_data, case ApplData#appl_data.mod of [] -> {ok, undefined}; _ -> %% Name = ApplData#appl_data.name, Running = S#state.running, foreach( fun(AppName) -> case lists:keymember(AppName, 1, Running) of true -> ok; false -> throw({info, {not_running, AppName}}) end end, Appl#appl.apps), case application_master:start_link(ApplData, Type) of {ok, _Pid} = Ok -> Ok; {error, _Reason} = Error -> throw(Error) end end. %%----------------------------------------------------------------- %% Stop application locally. %%----------------------------------------------------------------- stop_appl(AppName, Id, Type) when is_pid(Id) -> unlink(Id), application_master:stop(Id), info_exited(AppName, stopped, Type), ets:delete(ac_tab, {application_master, AppName}); stop_appl(AppName, undefined, Type) -> %% Code-only application stopped info_exited(AppName, stopped, Type); stop_appl(_AppName, _Id, _Type) -> %% Distributed application stopped ok. keysearchdelete(Key, Pos, List) -> ksd(Key, Pos, List, []). ksd(Key, Pos, [H | T], Rest) when element(Pos, H) =:= Key -> {value, H, Rest ++ T}; ksd(Key, Pos, [H | T], Rest) -> ksd(Key, Pos, T, [H | Rest]); ksd(_Key, _Pos, [], _Rest) -> false. keyreplaceadd(Key, Pos, List, New) -> %% Maintains the order! case lists:keymember(Key, Pos, List) of true -> keyreplace(Key, Pos, List, New); false -> [New | List] end. validRestartType(permanent) -> true; validRestartType(temporary) -> true; validRestartType(transient) -> true; validRestartType(RestartType) -> throw({error, {invalid_restart_type, RestartType}}). nd({distributed, Node}) -> Node; nd(_) -> node(). get_restart_type(undefined, OldRT) -> OldRT; get_restart_type(RT, _OldRT) -> RT. get_appl_name(Name) when is_atom(Name) -> Name; get_appl_name({application, Name, _}) when is_atom(Name) -> Name; get_appl_name(Appl) -> throw({error, {bad_application, Appl}}). make_appl(Name) when is_atom(Name) -> FName = atom_to_list(Name) ++ ".app", case code:where_is_file(FName) of non_existing -> {error, {file:format_error(enoent), FName}}; FullName -> case prim_consult(FullName) of {ok, [Application]} -> {ok, make_appl_i(Application)}; {error, Reason} -> {error, {file:format_error(Reason), FName}}; error -> {error, "bad encoding"} end end; make_appl(Application) -> {ok, make_appl_i(Application)}. prim_consult(FullName) -> case erl_prim_loader:get_file(FullName) of {ok, Bin, _} -> case file_binary_to_list(Bin) of {ok, String} -> case erl_scan:string(String) of {ok, Tokens, _EndLine} -> prim_parse(Tokens, []); {error, Reason, _EndLine} -> {error, Reason} end; error -> error end; error -> {error, enoent} end. prim_parse(Tokens, Acc) -> case lists:splitwith(fun(T) -> element(1,T) =/= dot end, Tokens) of {[], []} -> {ok, lists:reverse(Acc)}; {Tokens2, [{dot,_} = Dot | Rest]} -> case erl_parse:parse_term(Tokens2 ++ [Dot]) of {ok, Term} -> prim_parse(Rest, [Term | Acc]); {error, _R} = Error -> Error end; {Tokens2, []} -> case erl_parse:parse_term(Tokens2) of {ok, Term} -> {ok, lists:reverse([Term | Acc])}; {error, _R} = Error -> Error end end. make_appl_i({application, Name, Opts}) when is_atom(Name), is_list(Opts) -> Descr = get_opt(description, Opts, ""), Id = get_opt(id, Opts, ""), Vsn = get_opt(vsn, Opts, ""), Mods = get_opt(modules, Opts, []), Regs = get_opt(registered, Opts, []), Apps = get_opt(applications, Opts, []), Mod = case get_opt(mod, Opts, []) of {M,_A}=MA when is_atom(M) -> MA; [] -> []; Other -> throw({error, {badstartspec, Other}}) end, Phases = get_opt(start_phases, Opts, undefined), Env = get_opt(env, Opts, []), MaxP = get_opt(maxP, Opts, infinity), MaxT = get_opt(maxT, Opts, infinity), IncApps = get_opt(included_applications, Opts, []), {#appl_data{name = Name, regs = Regs, mod = Mod, phases = Phases, mods = Mods, inc_apps = IncApps, maxP = MaxP, maxT = MaxT}, Env, IncApps, Descr, Id, Vsn, Apps}; make_appl_i({application, Name, Opts}) when is_list(Opts) -> throw({error,{invalid_name,Name}}); make_appl_i({application, _Name, Opts}) -> throw({error,{invalid_options, Opts}}); make_appl_i(Appl) -> throw({error, {bad_application, Appl}}). %%----------------------------------------------------------------- %% Merge current applications with changes. %%----------------------------------------------------------------- %% do_change_apps(Applications, Config, OldAppls) -> NewAppls %% Applications = [{application, AppName, [{Key,Value}]}] %% Config = [{AppName,[{Par,Value}]} | File] %% OldAppls = NewAppls = [#appl{}] do_change_apps(Applications, Config, OldAppls) -> %% OTP-4867 %% Config = contents of sys.config file %% May now contain names of other .config files as well as %% configuration parameters. %% Therefore read and merge contents. {ok, SysConfig, Errors} = check_conf_sys(Config), %% Report errors, but do not terminate %% (backwards compatible behaviour) lists:foreach(fun({error, {SysFName, Line, Str}}) -> ?LOG_ERROR("~tp: ~w: ~ts~n",[SysFName, Line, Str], #{error_logger=>#{tag=>error}}) end, Errors), {map(fun(Appl) -> AppName = Appl#appl.name, case is_loaded_app(AppName, Applications) of {true, Application} -> do_change_appl(make_appl(Application), Appl, SysConfig); %% ignored removed apps - handled elsewhere false -> Appl end end, OldAppls), SysConfig}. is_loaded_app(AppName, [{application, AppName, App} | _]) -> {true, {application, AppName, App}}; is_loaded_app(AppName, [_ | T]) -> is_loaded_app(AppName, T); is_loaded_app(_AppName, []) -> false. do_change_appl({ok, {ApplData, Env, IncApps, Descr, Id, Vsn, Apps}}, OldAppl, Config) -> AppName = OldAppl#appl.name, %% Merge application env with env from sys.config, if any ConfEnv = get_opt(AppName, Config, []), NewEnv1 = merge_app_env(Env, ConfEnv), %% Merge application env with command line arguments, if any CmdLineEnv = get_cmd_env(AppName), NewEnv2 = merge_app_env(NewEnv1, CmdLineEnv), %% Update ets table with new application env del_env(AppName), add_env(AppName, NewEnv2), OldAppl#appl{appl_data=ApplData, descr=Descr, id=Id, vsn=Vsn, inc_apps=IncApps, apps=Apps}; do_change_appl({error, _R} = Error, _Appl, _ConfData) -> throw(Error). get_opt(Key, List, Default) -> case lists:keyfind(Key, 1, List) of {_Key, Val} -> Val; _ -> Default end. get_cmd_env(Name) -> case init:get_argument(Name) of {ok, Args} -> foldl(fun(List, Res) -> conv(List) ++ Res end, [], Args); _ -> [] end. conv([Key, Val | T]) -> [{make_term(Key), make_term(Val)} | conv(T)]; conv(_) -> []. make_term(Str) -> case erl_scan:string(Str) of {ok, Tokens, _} -> case erl_parse:parse_term(Tokens ++ [{dot, erl_anno:new(1)}]) of {ok, Term} -> Term; {error, {_,M,Reason}} -> handle_make_term_error(M, Reason, Str) end; {error, {_,M,Reason}, _} -> handle_make_term_error(M, Reason, Str) end. handle_make_term_error(Mod, Reason, Str) -> ?LOG_ERROR("application_controller: ~ts: ~ts~n", [Mod:format_error(Reason), Str], #{error_logger=>#{tag=>error}}), throw({error, {bad_environment_value, Str}}). get_env_i(Name, #state{conf_data = ConfData}) when is_list(ConfData) -> case lists:keyfind(Name, 1, ConfData) of {_Name, Env} -> Env; _ -> [] end; get_env_i(_Name, _) -> []. %% Merges envs for all apps. Env2 overrides Env1 merge_env(Env1, Env2) -> merge_env(Env1, Env2, []). merge_env([{App, AppEnv1} | T], Env2, Res) -> case get_env_key(App, Env2) of {value, AppEnv2, RestEnv2} -> NewAppEnv = merge_app_env(AppEnv1, AppEnv2), merge_env(T, RestEnv2, [{App, NewAppEnv} | Res]); _ -> merge_env(T, Env2, [{App, AppEnv1} | Res]) end; merge_env([], Env2, Res) -> Env2 ++ Res. %% Changes the environment for the given application %% If there is no application, an empty one is created change_app_env(Env, App, Fun) -> case get_env_key(App, Env) of {value, AppEnv, RestEnv} -> [{App, Fun(AppEnv)} | RestEnv]; _ -> [{App, Fun([])} | Env] end. %% Merges envs for an application. Env2 overrides Env1 merge_app_env(Env1, Env2) -> merge_app_env(Env1, Env2, []). merge_app_env([{Key, Val} | T], Env2, Res) -> case get_env_key(Key, Env2) of {value, NewVal, RestEnv} -> merge_app_env(T, RestEnv, [{Key, NewVal}|Res]); _ -> merge_app_env(T, Env2, [{Key, Val} | Res]) end; merge_app_env([], Env2, Res) -> Env2 ++ Res. get_env_key(Key, Env) -> get_env_key(Env, Key, []). get_env_key([{Key, Val} | T], Key, Res) -> {value, Val, T ++ Res}; get_env_key([H | T], Key, Res) -> get_env_key(T, Key, [H | Res]); get_env_key([], _Key, Res) -> Res. add_env(Name, Env) -> foreach(fun({Key, Value}) -> ets:insert(ac_tab, {{env, Name, Key}, Value}) end, Env). del_env(Name) -> ets:match_delete(ac_tab, {{env, Name, '_'}, '_'}). check_user() -> case whereis(user) of User when is_pid(User) -> group_leader(User, self()); _ -> ok end. %%----------------------------------------------------------------- %% Prepare for a release upgrade by reading all the evironment variables. %%----------------------------------------------------------------- do_prep_config_change(Apps) -> do_prep_config_change(Apps, []). do_prep_config_change([], EnvBefore) -> EnvBefore; do_prep_config_change([{App, _Id} | Apps], EnvBefore) -> Env = application:get_all_env(App), do_prep_config_change(Apps, [{App, Env} | EnvBefore]). %%----------------------------------------------------------------- %% Inform all running applications about the changed configuration. %%----------------------------------------------------------------- do_config_change(Apps, EnvBefore) -> do_config_change(Apps, EnvBefore, []). do_config_change([], _EnvBefore, []) -> ok; do_config_change([], _EnvBefore, Errors) -> {error, Errors}; do_config_change([{App, _Id} | Apps], EnvBefore, Errors) -> AppEnvNow = lists:sort(application:get_all_env(App)), AppEnvBefore = case lists:keyfind(App, 1, EnvBefore) of false -> []; {App, AppEnvBeforeT} -> lists:sort(AppEnvBeforeT) end, Res = case AppEnvNow of AppEnvBefore -> ok; _ -> case do_config_diff(AppEnvNow, AppEnvBefore) of {[], [], []} -> ok; {Changed, New, Removed} -> case application:get_key(App, mod) of {ok, {Mod, _Para}} -> case catch Mod:config_change(Changed, New, Removed) of ok -> ok; %% It is not considered as an error %% if the cb-function is not defined {'EXIT', {undef, _}} -> ok; {error, _} = Error -> Error; Else -> {error, Else} end; {ok, []} -> {error, {module_not_defined, App}}; undefined -> {error, {application_not_found, App}} end end end, case Res of ok -> do_config_change(Apps, EnvBefore, Errors); {error, NewError} -> do_config_change(Apps, EnvBefore,[NewError | Errors]) end. %%----------------------------------------------------------------- %% Check if the configuration is changed in anyway. %%----------------------------------------------------------------- do_config_diff(AppEnvNow, AppEnvBefore) -> do_config_diff(AppEnvNow, AppEnvBefore, {[], []}). do_config_diff([], AppEnvBefore, {Changed, New}) -> Removed = lists:foldl(fun({Env, _Value}, Acc) -> [Env | Acc] end, [], AppEnvBefore), {Changed, New, Removed}; do_config_diff(AppEnvNow, [], {Changed, New}) -> {Changed, AppEnvNow++New, []}; do_config_diff([{Env, Value} | AppEnvNow], AppEnvBefore, {Changed, New}) -> case lists:keyfind(Env, 1, AppEnvBefore) of {Env, Value} -> do_config_diff(AppEnvNow, lists:keydelete(Env,1,AppEnvBefore), {Changed, New}); {Env, _OtherValue} -> do_config_diff(AppEnvNow, lists:keydelete(Env,1,AppEnvBefore), {[{Env, Value} | Changed], New}); false -> do_config_diff(AppEnvNow, AppEnvBefore, {Changed, [{Env, Value}|New]}) end. %%----------------------------------------------------------------- %% Read the .config files. %%----------------------------------------------------------------- check_conf() -> case init:get_argument(config) of {ok, Files} -> {ok, lists:foldl( fun(File, Env) -> BFName = filename:basename(File,".config"), FName = filename:join(filename:dirname(File), BFName ++ ".config"), case load_file(FName) of {ok, NewEnv} -> %% OTP-4867 %% sys.config may now contain names of %% other .config files as well as %% configuration parameters. %% Therefore read and merge contents. if BFName =:= "sys" -> DName = filename:dirname(FName), {ok, SysEnv, Errors} = check_conf_sys(NewEnv, [], [], DName), %% Report first error, if any, and %% terminate %% (backwards compatible behaviour) case Errors of [] -> merge_env(Env, SysEnv); [{error, {SysFName, Line, Str}}|_] -> throw({error, {SysFName, Line, Str}}) end; true -> merge_env(Env, NewEnv) end; {error, {Line, _Mod, Str}} -> throw({error, {FName, Line, Str}}) end end, [], lists:append(Files))}; _ -> {ok, []} end. check_conf_sys(Env) -> check_conf_sys(Env, [], [], []). check_conf_sys([File|T], SysEnv, Errors, DName) when is_list(File),is_list(DName) -> BFName = filename:basename(File, ".config"), FName = filename:join(filename:dirname(File), BFName ++ ".config"), LName = case filename:pathtype(FName) of relative when (DName =/= []) -> % Check if relative to sys.config dir otherwise use legacy mode, % i.e relative to cwd. RName = filename:join(DName, FName), case erl_prim_loader:read_file_info(RName) of {ok, _} -> RName ; error -> FName end; _ -> FName end, case load_file(LName) of {ok, NewEnv} -> check_conf_sys(T, merge_env(SysEnv, NewEnv), Errors, DName); {error, {Line, _Mod, Str}} -> check_conf_sys(T, SysEnv, [{error, {LName, Line, Str}}|Errors], DName) end; check_conf_sys([Tuple|T], SysEnv, Errors, DName) -> check_conf_sys(T, merge_env(SysEnv, [Tuple]), Errors, DName); check_conf_sys([], SysEnv, Errors, _) -> {ok, SysEnv, lists:reverse(Errors)}. load_file(File) -> %% We can't use file:consult/1 here. Too bad. case erl_prim_loader:get_file(File) of {ok, Bin, _FileName} -> %% Make sure that there is some whitespace at the end of the string %% (so that reading a file with no NL following the "." will work). case file_binary_to_list(Bin) of {ok, String} -> scan_file(String ++ " "); error -> {error, {none, scan_file, "bad encoding"}} end; error -> {error, {none, open_file, "configuration file not found"}} end. scan_file(Str) -> case erl_scan:tokens([], Str, 1) of {done, {ok, Tokens, _}, Left} -> case erl_parse:parse_term(Tokens) of {ok,L}=Res when is_list(L) -> case only_ws(Left) of true -> Res; false -> %% There was trailing garbage found after the list. config_error() end; {ok,_} -> %% Parsing succeeded but the result is not a list. config_error(); Error -> Error end; {done, Result, _} -> {error, {none, parse_file, tuple_to_list(Result)}}; {more, _} -> {error, {none, load_file, "no ending found"}} end. only_ws([C|Cs]) when C =< $\s -> only_ws(Cs); only_ws([$%|Cs]) -> only_ws(strip_comment(Cs)); % handle comment only_ws([_|_]) -> false; only_ws([]) -> true. strip_comment([$\n|Cs]) -> Cs; strip_comment([_|Cs]) -> strip_comment(Cs); strip_comment([]) -> []. config_error() -> {error, {none, load_file, "configuration file must contain ONE list ended by "}}. %%----------------------------------------------------------------- %% Info messages sent to logger %%----------------------------------------------------------------- info_started(Name, Node) -> ?LOG_INFO(#{label=>{application_controller,progress}, report=>[{application, Name}, {started_at, Node}]}, #{domain=>[otp,sasl], report_cb=>fun application_controller:format_log/2, logger_formatter=>#{title=>"PROGRESS REPORT"}, error_logger=>#{tag=>info_report, type=>progress, report_cb=> fun application_controller:format_log/1}}). info_exited(Name, Reason, Type) -> ?LOG_NOTICE(#{label=>{application_controller,exit}, report=>[{application, Name}, {exited, Reason}, {type, Type}]}, #{domain=>[otp], report_cb=>fun application_controller:format_log/2, error_logger=>#{tag=>info_report, type=>std_info, report_cb=> fun application_controller:format_log/1}}). %% format_log/1 is the report callback used by Logger handler %% error_logger only. It is kept for backwards compatibility with %% legacy error_logger event handlers. This function must always %% return {Format,Args} compatible with the arguments in this module's %% calls to error_logger prior to OTP-21.0. format_log(LogReport) -> Depth = error_logger:get_format_depth(), FormatOpts = #{chars_limit => unlimited, depth => Depth, single_line => false, encoding => utf8}, format_log_multi(limit_report(LogReport, Depth), FormatOpts). limit_report(LogReport, unlimited) -> LogReport; limit_report(#{label:={application_controller,progress}, report:=[{application,_}=Application, {started_at,Node}]}=LogReport, Depth) -> LogReport#{report=>[Application, {started_at,io_lib:limit_term(Node, Depth)}]}; limit_report(#{label:={application_controller,exit}, report:=[{application,_}=Application, {exited,Reason},{type,Type}]}=LogReport, Depth) -> LogReport#{report=>[Application, {exited,io_lib:limit_term(Reason, Depth)}, {type,io_lib:limit_term(Type, Depth)}]}. %% format_log/2 is the report callback for any Logger handler, except %% error_logger. format_log(Report, FormatOpts0) -> Default = #{chars_limit => unlimited, depth => unlimited, single_line => false, encoding => utf8}, FormatOpts = maps:merge(Default, FormatOpts0), IoOpts = case FormatOpts of #{chars_limit:=unlimited} -> []; #{chars_limit:=Limit} -> [{chars_limit,Limit}] end, {Format,Args} = format_log_single(Report, FormatOpts), io_lib:format(Format, Args, IoOpts). format_log_single(#{label:={application_controller,progress}, report:=[{application,Name},{started_at,Node}]}, #{single_line:=true,depth:=Depth}=FormatOpts) -> P = p(FormatOpts), Format = "Application: "++P++". Started at: "++P++".", Args = case Depth of unlimited -> [Name,Node]; _ -> [Name,Depth,Node,Depth] end, {Format,Args}; format_log_single(#{label:={application_controller,exit}, report:=[{application,Name}, {exited,Reason}, {type,Type}]}, #{single_line:=true,depth:=Depth}=FormatOpts) -> P = p(FormatOpts), Format = lists:append(["Application: ",P,". Exited: ",P, ". Type: ",P,"."]), Args = case Depth of unlimited -> [Name,Reason,Type]; _ -> [Name,Depth,Reason,Depth,Type,Depth] end, {Format,Args}; format_log_single(Report,FormatOpts) -> format_log_multi(Report,FormatOpts). format_log_multi(#{label:={application_controller,progress}, report:=[{application,Name}, {started_at,Node}]}, #{depth:=Depth}=FormatOpts) -> P = p(FormatOpts), Format = lists:append( [" application: ",P,"~n", " started_at: ",P,"~n"]), Args = case Depth of unlimited -> [Name,Node]; _ -> [Name,Depth,Node,Depth] end, {Format,Args}; format_log_multi(#{label:={application_controller,exit}, report:=[{application,Name}, {exited,Reason}, {type,Type}]}, #{depth:=Depth}=FormatOpts) -> P = p(FormatOpts), Format = lists:append( [" application: ",P,"~n", " exited: ",P,"~n", " type: ",P,"~n"]), Args = case Depth of unlimited -> [Name,Reason,Type]; _ -> [Name,Depth,Reason,Depth,Type,Depth] end, {Format,Args}. p(#{single_line:=Single,depth:=Depth,encoding:=Enc}) -> "~"++single(Single)++mod(Enc)++p(Depth); p(unlimited) -> "p"; p(_Depth) -> "P". single(true) -> "0"; single(false) -> "". mod(latin1) -> ""; mod(_) -> "t". %%----------------------------------------------------------------- %% Reply to all processes waiting this application to be started. %%----------------------------------------------------------------- reply_to_requester(AppName, Start_req, Res) -> R = case Res of {ok, _Id} -> ok; {info, Reason} -> {error, Reason}; Error -> Error end, lists:foldl(fun(Sp, AccIn) -> case Sp of {AppName, From} -> reply(From, R), AccIn; _ -> [Sp | AccIn] end end, [], Start_req). %%----------------------------------------------------------------- %% Update the environment variable permission for an application. %%----------------------------------------------------------------- update_permissions(AppName, Bool) -> T = {env, kernel, permissions}, case ets:lookup(ac_tab, T) of [] -> ets:insert(ac_tab, {T, [{AppName, Bool}]}); [{_, Perm}] -> Perm2 = lists:keydelete(AppName, 1, Perm), ets:insert(ac_tab, {T, [{AppName, Bool}|Perm2]}) end. %%----------------------------------------------------------------- %% These functions are only to be used from testsuites. %%----------------------------------------------------------------- test_change_apps(Apps, Conf) -> Res = test_make_apps(Apps, []), test_do_change_appl(Apps, Conf, Res). test_do_change_appl([], _, _) -> ok; test_do_change_appl([A|Apps], [], [R|Res]) -> _ = do_change_appl(R, #appl{name = A}, []), test_do_change_appl(Apps, [], Res); test_do_change_appl([A|Apps], [C|Conf], [R|Res]) -> _ = do_change_appl(R, #appl{name = A}, C), test_do_change_appl(Apps, Conf, Res). test_make_apps([], Res) -> lists:reverse(Res); test_make_apps([A|Apps], Res) -> test_make_apps(Apps, [make_appl(A) | Res]). file_binary_to_list(Bin) -> Enc = case epp:read_encoding_from_binary(Bin) of none -> epp:default_encoding(); Encoding -> Encoding end, case catch unicode:characters_to_list(Bin, Enc) of String when is_list(String) -> {ok, String}; _ -> error end. %%----------------------------------------------------------------- %% String conversion %% Exit reason needs to be a printable string %% (and of length <200, but init now does the chopping). %%----------------------------------------------------------------- -spec to_string(term()) -> string(). to_string(Term) -> case io_lib:printable_list(Term) of true -> Term; false -> lists:flatten(io_lib:format("~0p", [Term])) end.