%% -*- coding: utf-8 -*-
%% Copyright (c) 2022, Madalin Grigore-Enescu <https://github.com/ergenius> <https://ergenius.com>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(t__srv).
-author("Madalin Grigore-Enescu").
-behaviour(gen_server).
-include("../include/t__.hrl").
%% API
-export([start_link/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([config_set_call/3, config_set_cast/2]).
-export([config_delete_call/2, config_delete_cast/1]).
-record(t__srv_state, {
configs = [] :: proplists:proplist(),
dev_count = 0,
timer_ref = undefined,
timer_interval = 10000
}).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% API
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec start_link(Args :: proplists:proplist()) -> {'ok', pid()} | 'ignore' | {'error', term()}.
%% @doc Creates a gen_server process as part of a supervision tree.
%% The function should be called, directly or indirectly, by the supervisor.
%% It will, among other things, ensure that the gen_server is linked to the supervisor.
start_link(Args) -> gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
%% @doc Config set
%% See t__:config_set/3 for more details
config_set_call(Application, Config, Timeout) -> gen_server:call(?MODULE, {config_set, Application, Config}, Timeout).
%% @doc Config set
%% See t__:config_set_cast/3 for more details
config_set_cast(Application, Config) -> gen_server:cast(?MODULE, {config_set, Application, Config}).
%% @doc Config delete
%% See t__:config_delete/2 for more details
config_delete_call(Application, Timeout) -> gen_server:call(?MODULE, {config_delete, Application}, Timeout).
%% @doc Config delete
%% See t__:config_delete_cast/2 for more details
config_delete_cast(Application) -> gen_server:cast(?MODULE, {config_delete, Application}).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% INIT
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Init gen_server
%% Whenever a gen_server process is started using start/3,4 or start_link/3,4, this function is called by the new process to initialize.
%%
%% Args is the Args argument provided to the start function.
%% If the initialization is successful, the function is to return {ok,State}, {ok,State,Timeout}, or {ok,State,hibernate},
%% where State is the internal state of the gen_server process.
%%
%% If an integer time-out value is provided, a time-out occurs unless a request or a message is received within Timeout milliseconds.
%% A time-out is represented by the atom timeout, which is to be handled by the Module:handle_info/2 callback function.
%% The atom infinity can be used to wait indefinitely, this is the default value.
%%
%% If hibernate is specified instead of a time-out value, the process goes into hibernation when waiting for the next message to arrive
%% (by calling proc_lib:hibernate/3).
%%
%% If the initialization fails, the function is to return {stop, Reason}, where Reason is any term, or ignore.
init(Args) -> private_handle_init(Args).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% handle_call
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Handle gen_server calls
%% Whenever a gen_server process receives a request sent using call/2,3 or multi_call/2,3,4, this function is called to handle the request.
%%
%% Request is the Request argument provided to call or multi_call.
%% From is a tuple {Pid,Tag}, where Pid is the pid of the client and Tag is a unique tag.
%% State is the internal state of the gen_server process.
%%
%% If {reply,Reply,NewState} is returned, {reply,Reply,NewState,Timeout} or {reply,Reply,NewState,hibernate},
%% Reply is given back to From as the return value of call/2,3 or included in the return value of multi_call/2,3,4.
%% The gen_server process then continues executing with the possibly updated internal state NewState.
%%
%% For a description of Timeout and hibernate, see Module:init/1.
%%
%% If {noreply,NewState} is returned, {noreply,NewState,Timeout}, or {noreply,NewState,hibernate},
%% the gen_server process continues executing with NewState. Any reply to From must be specified explicitly using reply/2.
%%
%% If {stop,Reason,Reply,NewState} is returned, Reply is given back to From.
%%
%% If {stop,Reason,NewState} is returned, any reply to From must be specified explicitly using reply/2.
%% The gen_server process then calls Module:terminate(Reason,NewState) and terminates.
%% config_set
handle_call({config_set, Application, Config = #t__config{}}, _From,
State = #t__srv_state{}) when erlang:is_atom(Application) ->
{Reply, NewState} = private_handle_config_set(Application, Config, State),
{reply, Reply, NewState};
%% config_delete
handle_call({config_delete, Application}, _From,
State = #t__srv_state{}) when erlang:is_atom(Application) ->
{Reply, NewState} = private_handle_config_delete(Application, State),
{reply, Reply, NewState};
%% Handle any other call
handle_call(_Msg, _From, State) -> {reply, error, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% handle_cast
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Handle gen_server casts
%% Whenever a gen_server process receives a request sent using cast/2 or abcast/2,3, this function is called to handle the request.
%% For a description of the arguments and possible return values, see Module:handle_call/3.
%% config_set
handle_cast({config_set, Application, Config = #t__config{}},
State = #t__srv_state{}) when erlang:is_atom(Application) ->
{_, NewState} = private_handle_config_set(Application, Config, State),
{noreply, NewState};
%% config_delete
handle_cast({config_delete, Application},
State = #t__srv_state{}) when erlang:is_atom(Application) ->
{_, NewState} = private_handle_config_delete(Application, State),
{noreply, NewState};
%% Handle any other cast
handle_cast(_Msg, State) -> {noreply, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% handle_info
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Handle gen_server info
%% This function is called by a gen_server process when a time-out occurs or when it receives any other message
%% than a synchronous or asynchronous request (or a system message).
%% Info is either the atom timeout, if a time-out has occurred, or the received message.
%% For a description of the other arguments and possible return values, see Module:handle_call/3.
%% Handle monitor
handle_info(monitor, State) -> private_handle_monitor(State);
%% Handle any other info
handle_info(_Info, State) -> {noreply, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% terminate/code_change
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Handle gen_server terminate
%%
%% This function is called by a gen_server process when it is about to terminate.
%% It is to be the opposite of Module:init/1 and do any necessary cleaning up.
%% When it returns, the gen_server process terminates with Reason. The return value is ignored.
%%
%% Reason is a term denoting the stop reason and State is the internal state of the gen_server process.
%%
%% Reason depends on why the gen_server process is terminating. If it is because another callback function has returned a stop tuple {stop,..},
%% Reason has the value specified in that tuple. If it is because of a failure, Reason is the error reason.
%%
%% If the gen_server process is part of a supervision tree and is ordered by its supervisor to terminate, this function is called with Reason=shutdown
%% if the following conditions apply:
%% - The gen_server process has been set to trap exit signals.
%% - The shutdown strategy as defined in the child specification of the supervisor is an integer time-out value, not brutal_kill.
%%
%% Even if the gen_server process is not part of a supervision tree, this function is called if it receives an 'EXIT' message from its parent.
%% Reason is the same as in the 'EXIT' message.
%% Otherwise, the gen_server process terminates immediately.
%%
%% Notice that for any other reason than normal, shutdown, or {shutdown,Term}, the gen_server process is assumed to terminate
%% because of an error and an error report is issued using error_logger:format/2.
terminate(_Reason, State) -> private_handle_terminate(State).
%% @doc Handle gen_server code change
%% This function is called by a gen_server process when it is to update its internal state during a release upgrade/downgrade,
%% that is, when the instruction {update,Module,Change,...}, where Change={advanced,Extra}, is specifed in the appup file.
%% For more information, see section Release Handling Instructions in OTP Design Principles.
%%
%% For an upgrade, OldVsn is Vsn, and for a downgrade, OldVsn is {down,Vsn}. Vsn is defined by the vsn attribute(s) of the old version
%% of the callback module Module. If no such attribute is defined, the version is the checksum of the Beam file.
%%
%% State is the internal state of the gen_server process.
%% Extra is passed "as is" from the {advanced,Extra} part of the update instruction.
%%
%% If successful, the function must return the updated internal state.
%% If the function returns {error,Reason}, the ongoing upgrade fails and rolls back to the old release.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% private_handle
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%=============================================================
%% private_handle_init
%%=============================================================
%% @doc Init gen_server
private_handle_init(_Args) -> private_handle_init(application:loaded_applications(), #t__srv_state{}).
private_handle_init([{Application, _Description, _Vsn} | T],
State = #t__srv_state{
configs = Configs
}) ->
case t__:config_get_environment(Application) of
{ok, Config} ->
%% Add configured applications
private_handle_init(T, State#t__srv_state{
configs = [{Application, Config} | Configs]
});
_ ->
%% Skip applications not configured
private_handle_init(T, State)
end;
private_handle_init([], State) ->
{ok, private_state_on_change(private_update_applications(State)), infinity}.
%%=============================================================
%% private_handle_config_set
%%=============================================================
%% @doc config_set
private_handle_config_set(Application, Config = #t__config{}, State = #t__srv_state{}) ->
{_, DeleteState} = private_handle_config_delete(Application, State),
DeleteStateConfigs = DeleteState#t__srv_state.configs,
{ok, private_state_on_change(private_update_applications(DeleteState#t__srv_state{
configs = [{Application, Config} | DeleteStateConfigs]
}))}.
%%=============================================================
%% private_handle_config_delete
%%=============================================================
%% @doc Delete specified application configuration
private_handle_config_delete(Application, State = #t__srv_state{
configs = Configs
}) ->
case proplists:get_value(Application, Configs, undefined) of
#t__config{
repositories = Repositories
} ->
private_delete_repositories(Application, Repositories),
NewConfigs = proplists:delete(Application, Configs),
application:unset_env(Application, t__),
{ok, private_state_on_change(State#t__srv_state{configs = NewConfigs})};
_ ->
{{error, not_found}, State}
end.
%%=============================================================
%% private_handle_monitor
%%=============================================================
%% @doc Monitor all configured applications
private_handle_monitor(State) ->
NewState = private_update_applications(State),
{noreply, private_state_on_change(NewState#t__srv_state{
timer_ref = undefined
})}.
%%=============================================================
%% private_handle_terminate
%%=============================================================
%% @doc Handle gen_server terminate
private_handle_terminate(State = #t__srv_state{
configs = [{Application, #t__config{
repositories = Repositories}}|T]}) ->
private_delete_repositories(Application, Repositories),
private_handle_terminate(State#t__srv_state{configs =T});
private_handle_terminate(#t__srv_state{}) ->
%% POT update
ok.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% private_
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%=============================================================
%% private_update_applications
%%=============================================================
%% @doc Update all applications
private_update_applications(State=#t__srv_state{configs = Configs}) ->
State#t__srv_state{configs = private_update_applications(Configs, [])}.
private_update_applications([{ApplicationName, Config}|T], Acum) ->
NewConfig = private_update_application(ApplicationName, Config),
private_update_applications(T, [{ApplicationName, NewConfig}|Acum]);
private_update_applications([], Acum) -> Acum.
%% @doc Iterate all application repositories and update them
%% Ignore applications without repositories and handle only applications that have repositories
private_update_application(_ApplicationName, Config = #t__config{repositories = undefined}) ->
Config;
private_update_application(ApplicationName, Config = #t__config{
repositories = Repositories
}) when erlang:is_list(Repositories) ->
Config#t__config{
repositories = private_update_application(ApplicationName, Repositories, [])
}.
%% @doc Update the specified application
private_update_application(ApplicationName, [Repository = #t__repository{}|T], Acum) ->
case t__repository:update(ApplicationName, Repository) of
{ok, NewRepository} -> private_update_application(ApplicationName, T, [NewRepository|Acum]);
Error ->
?T__LOG(error, "Updating application repository failed with error."
" Repository directory does not exist or it is not accesible.",
[
{application, ApplicationName},
{repository, Repository},
{error, Error}
]),
%% We are facing 3 choices here:
%% 1. exit()
%% 2. keep the repository and generate errors on each timer info
%% 3. delete the repository and silence the errors
%%
%% Do not forget we may encounter this situation only when development mode is enabled.
%% I do believe it's better to chose 2 on a developer setup. Maybe the DEV can fix the problem
%% without us crushing the application and continue his work of changing repositories
%% so... do nothing about it!
private_update_application(ApplicationName, T, [Repository|Acum])
end;
private_update_application(_ApplicationName, _, Acum) -> Acum.
%%=============================================================
%% private_state_on_change
%%=============================================================
%% @doc Update state on configs change
private_state_on_change(State = #t__srv_state{configs = Configs, timer_ref = TimerRef, timer_interval = TimerInterval}) ->
%% Count configs with dev enabled
NewDevCount = private_state_on_change_dev_count(Configs),
%% Start a new timer for monitoring PO file changes if necessary
NewTimerRef = case {NewDevCount > 0, TimerRef} of
%% We don't have a timer, create new
{true, undefined} -> erlang:send_after(TimerInterval, self(), monitor);
%% We already have a timer, return it
{true, _} -> TimerRef;
%% Nothing left to monitor, no timer
{false, undefined} -> undefined;
%% Nothing left to monitor, stop existing timer
{false, _} -> erlang:cancel_timer(TimerRef, [{async, true}]), undefined
end,
%%?T__LOG(debug, "t__srv state changed", [State#t__srv_state{dev_count = NewDevCount, timer_ref = NewTimerRef}]),
State#t__srv_state{dev_count = NewDevCount, timer_ref = NewTimerRef}.
%% @doc Count the dev in configs proplist
private_state_on_change_dev_count(Configs) -> private_state_on_change_dev_count(Configs, 0).
private_state_on_change_dev_count([{_, #t__config{dev = true}} | T], Acum) -> private_state_on_change_dev_count(T, Acum + 1);
private_state_on_change_dev_count([_ | T], Acum) -> private_state_on_change_dev_count(T, Acum);
private_state_on_change_dev_count([], Acum) -> Acum.
%%=============================================================
%% private_delete_repositories
%%=============================================================
%% @doc Delete the specified application repositories
private_delete_repositories(Application, [#t__repository{name=RepositoryName}|T]) ->
RepositoryEtsTable = t__repository:ets_table(Application, RepositoryName),
try ets:delete(RepositoryEtsTable)
catch _Exception:_Reason -> false
end,
private_delete_repositories(Application, T);
private_delete_repositories(_Application, []) -> ok.