Packages
nova
0.15.1
0.15.1
0.14.3
0.14.1
0.13.7
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.11
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.4.4
0.4.3
0.4.2
0.4.1
0.2.2
0.1.0
0.0.1
0.0.0
Nova is a web application framework
Current section
Files
Jump to
Current section
Files
src/nova_plugin_manager.erl
%%%-------------------------------------------------------------------
%%% @author Niclas Axelsson <niclas@burbas.se>
%%% @doc
%%% Main manager for handling plugins
%%% @end
%%% Created : 10 Aug 2024 by Niclas Axelsson <niclas@burbas.se>
%%%-------------------------------------------------------------------
-module(nova_plugin_manager).
-behaviour(gen_server).
%% API
-export([
start_link/0,
add_plugin/1,
add_plugin/3,
get_state/1,
set_state/2
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-include_lib("kernel/include/logger.hrl").
-define(TABLE, '__nova_plugins__').
-define(SERVER, ?MODULE).
-record(state, {}).
-record(plugin, {
module :: atom(),
name :: binary(),
version :: binary(),
state :: nova:state()
}).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%% @end
%%--------------------------------------------------------------------
-spec start_link() -> {ok, Pid :: pid()} |
{error, Error :: {already_started, pid()}} |
{error, Error :: term()} |
ignore.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% @doc
%% Adds a plugin to the manager. The plugin module must implement
%% the plugin_info/0 function returning #{title := Title, version := Version}.
%% Optionally it can implement init/0 returning the initial state
%% for the plugin, and stop/0 for cleaning up when the manager
%% terminates.
%% @end
-spec add_plugin({Class :: atom(), Module :: atom(), Opts :: map()} |
Module :: atom() | Callback :: function()) ->
ok | {error, Reason :: term()}.
add_plugin({_Class, Module, _Opts}) ->
add_plugin(Module);
add_plugin(Module) when is_atom(Module) ->
#{title := Title, version := Version} = case Module:plugin_info() of
{Title0, Version0, _, _, _} -> #{title => Title0, version => Version0};
PluginInfo -> PluginInfo
end,
add_plugin(Module, Title, Version);
add_plugin(Callback) when is_function(Callback) ->
Info = erlang:fun_info(Callback),
Module = proplists:get_value(module, Info),
add_plugin(Module).
-spec add_plugin(Module :: atom(), Name :: binary(), Version :: binary()) ->
ok | {error, Reason :: term()}.
add_plugin(Module, Name, Version) ->
case ets:lookup(?TABLE, Module) of
[#plugin{}] ->
?LOG_DEBUG(<<"Plugin ~p already initialized.">>, [Module]),
ok;
[] ->
gen_server:cast(?SERVER, {add_plugin, Module, Name, Version})
end.
%%--------------------------------------------------------------------
%% @doc
%% Gets the state of a plugin. The plugin can be specified
%% either as a module or as a callback function. The callback function
%% is used to extract the module name in order to retrieve the state.
%% @end
%%--------------------------------------------------------------------
-spec get_state(Module :: atom() | Callback :: function()) ->
{ok, State :: nova:state()} | {error, not_found}.
get_state(Module) when is_atom(Module) ->
case ets:lookup(?TABLE, Module) of
[#plugin{state = State}] ->
{ok, State};
_ ->
?LOG_DEBUG(<<"Plugin ~p not found. get_state/1 failed.">>, [Module]),
{error, not_found}
end;
get_state(Callback) when is_function(Callback) ->
Info = erlang:fun_info(Callback),
Module = proplists:get_value(module, Info),
get_state(Module).
%%--------------------------------------------------------------------
%% @doc
%% Sets the state of a plugin. The plugin can be specified
%% either as a module or as a callback function. The callback function
%% is used to extract the module name in order to set the state.
%% @end
%%--------------------------------------------------------------------
-spec set_state(Module :: atom() | Callback :: function(),
NewState :: term()) ->
ok | {error, not_found}.
set_state(Module, NewState) when is_atom(Module) ->
case ets:lookup(?TABLE, Module) of
[#plugin{} = P] ->
gen_server:call(?SERVER, {set_state, Module, P, NewState});
_ ->
?LOG_DEBUG(<<"Plugin ~p not found. set_state/2 failed.">>, [Module]),
{error, not_found}
end;
set_state(Callback, NewState) when is_function(Callback) ->
Info = erlang:fun_info(Callback),
Module = proplists:get_value(module, Info),
set_state(Module, NewState).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%% @end
%%--------------------------------------------------------------------
-spec init(Args :: term()) -> {ok, State :: term()} |
{ok, State :: term(), Timeout :: timeout()} |
{ok, State :: term(), hibernate} |
{stop, Reason :: term()} |
ignore.
init([]) ->
process_flag(trap_exit, true),
ets:new(?TABLE, [named_table, set, protected, {keypos, #plugin.module}]),
Plugins = nova_router:plugins(),
[ add_plugin(Callback) || Callback <- Plugins ],
{ok, #state{}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%% @end
%%--------------------------------------------------------------------
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
{reply, Reply :: term(), NewState :: term()} |
{reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
{reply, Reply :: term(), NewState :: term(), hibernate} |
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: term()} |
{stop, Reason :: term(), NewState :: term()}.
handle_call({set_state, Module, P, NewState}, _From, State) ->
?LOG_DEBUG(<<"Set state for plugin ~p; NewState: ~p">>, [Module, NewState]),
ets:insert(?TABLE, P#plugin{state = NewState}),
{reply, ok, State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(Request :: term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: term(), NewState :: term()}.
handle_cast({add_plugin, Module, Name, Version}, State) ->
PluginState =
case erlang:function_exported(Module, init, 0) of
true ->
?LOG_INFO(<<"Initializing plugin ~p">>, [Module]),
Module:init();
_ ->
?LOG_DEBUG(<<"Plugin ~p has no init/0 function. Defaulting to empty map #{}">>, [Module]),
#{}
end,
ets:insert(?TABLE, #plugin{module = Module, name = Name, version = Version, state = PluginState}),
{noreply, State};
handle_cast(_Request, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: normal | term(), NewState :: term()}.
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%% @end
%%--------------------------------------------------------------------
-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
State :: term()) -> any().
terminate(_Reason, _State) ->
%% Stop all plugins and clean state
Plugins = ets:tab2list(?TABLE),
[ Plugin:stop() || #plugin{module = Plugin} <- Plugins,
erlang:function_exported(Plugin, stop, 0) ],
ets:delete(?TABLE),
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%% @end
%%--------------------------------------------------------------------
-spec code_change(OldVsn :: term() | {down, term()},
State :: term(),
Extra :: term()) -> {ok, NewState :: term()} |
{error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================