|
|
@@ -0,0 +1,300 @@
|
|
|
+%%%-----------------------------------------------------------------------------
|
|
|
+%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved.
|
|
|
+%%%
|
|
|
+%%% Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
+%%% of this software and associated documentation files (the "Software"), to deal
|
|
|
+%%% in the Software without restriction, including without limitation the rights
|
|
|
+%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
+%%% copies of the Software, and to permit persons to whom the Software is
|
|
|
+%%% furnished to do so, subject to the following conditions:
|
|
|
+%%%
|
|
|
+%%% The above copyright notice and this permission notice shall be included in all
|
|
|
+%%% copies or substantial portions of the Software.
|
|
|
+%%%
|
|
|
+%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
+%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
+%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
+%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
+%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
+%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
+%%% SOFTWARE.
|
|
|
+%%%-----------------------------------------------------------------------------
|
|
|
+%%% @doc
|
|
|
+%%% emqttd plugins.
|
|
|
+%%%
|
|
|
+%%% @end
|
|
|
+%%%-----------------------------------------------------------------------------
|
|
|
+
|
|
|
+-module(emqttd_plugins).
|
|
|
+
|
|
|
+-author("Feng Lee <feng@emqtt.io>").
|
|
|
+
|
|
|
+-include("emqttd.hrl").
|
|
|
+
|
|
|
+-export([load/0, unload/0]).
|
|
|
+
|
|
|
+-export([load/1, unload/1]).
|
|
|
+
|
|
|
+-export([list/0]).
|
|
|
+
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+%% @doc Load all plugins when the broker started.
|
|
|
+%% @end
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+
|
|
|
+-spec load() -> list() | {error, any()}.
|
|
|
+load() ->
|
|
|
+ case env(loaded_file) of
|
|
|
+ {ok, File} ->
|
|
|
+ with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end);
|
|
|
+ undefined ->
|
|
|
+ %% No plugins available
|
|
|
+ ignore
|
|
|
+ end.
|
|
|
+
|
|
|
+with_loaded_file(File, SuccFun) ->
|
|
|
+ case read_loaded(File) of
|
|
|
+ {ok, Names} ->
|
|
|
+ SuccFun(Names);
|
|
|
+ {error, Error} ->
|
|
|
+ lager:error("Failed to read: ~p, error: ~p", [File, Error]),
|
|
|
+ {error, Error}
|
|
|
+ end.
|
|
|
+
|
|
|
+load_plugins(Names, Persistent) ->
|
|
|
+ Plugins = list(), NotFound = Names -- names(Plugins),
|
|
|
+ case NotFound of
|
|
|
+ [] -> ok;
|
|
|
+ NotFound -> lager:error("Cannot find plugins: ~p", [NotFound])
|
|
|
+ end,
|
|
|
+ NeedToLoad = Names -- NotFound -- names(started_app),
|
|
|
+ [load_plugin(find_plugin(Name, Plugins), Persistent) || Name <- NeedToLoad].
|
|
|
+
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+%% @doc Unload all plugins before broker stopped.
|
|
|
+%% @end
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+-spec unload() -> list() | {error, any()}.
|
|
|
+unload() ->
|
|
|
+ case env(loaded_file) of
|
|
|
+ {ok, File} ->
|
|
|
+ with_loaded_file(File, fun(Names) -> stop_plugins(Names) end);
|
|
|
+ undefined ->
|
|
|
+ ignore
|
|
|
+ end.
|
|
|
+
|
|
|
+%% stop plugins
|
|
|
+stop_plugins(Names) ->
|
|
|
+ [stop_app(App) || App <- Names].
|
|
|
+
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+%% @doc List all available plugins
|
|
|
+%% @end
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+-spec list() -> [mqtt_plugin()].
|
|
|
+list() ->
|
|
|
+ case env(plugins_dir) of
|
|
|
+ {ok, PluginsDir} ->
|
|
|
+ AppFiles = filelib:wildcard("*/ebin/*.app", PluginsDir),
|
|
|
+ Plugins = [plugin(PluginsDir, AppFile) || AppFile <- AppFiles],
|
|
|
+ StartedApps = names(started_app),
|
|
|
+ lists:map(fun(Plugin = #mqtt_plugin{name = Name}) ->
|
|
|
+ case lists:member(Name, StartedApps) of
|
|
|
+ true -> Plugin#mqtt_plugin{active = true};
|
|
|
+ false -> Plugin
|
|
|
+ end
|
|
|
+ end, Plugins);
|
|
|
+ undefined ->
|
|
|
+ []
|
|
|
+ end.
|
|
|
+
|
|
|
+plugin(PluginsDir, AppFile0) ->
|
|
|
+ AppFile = filename:join(PluginsDir, AppFile0),
|
|
|
+ {ok, [{application, Name, Attrs}]} = file:consult(AppFile),
|
|
|
+ CfgFile = filename:join([PluginsDir, Name, "etc/plugin.config"]),
|
|
|
+ AppsEnv1 =
|
|
|
+ case filelib:is_file(CfgFile) of
|
|
|
+ true ->
|
|
|
+ {ok, [AppsEnv]} = file:consult(CfgFile),
|
|
|
+ AppsEnv;
|
|
|
+ false ->
|
|
|
+ []
|
|
|
+ end,
|
|
|
+ Ver = proplists:get_value(vsn, Attrs, "0"),
|
|
|
+ Descr = proplists:get_value(description, Attrs, ""),
|
|
|
+ #mqtt_plugin{name = Name, version = Ver, config = AppsEnv1, descr = Descr}.
|
|
|
+
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+%% @doc Load One Plugin
|
|
|
+%% @end
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+
|
|
|
+-spec load(atom()) -> ok | {error, any()}.
|
|
|
+load(PluginName) when is_atom(PluginName) ->
|
|
|
+ case lists:member(PluginName, names(started_app)) of
|
|
|
+ true ->
|
|
|
+ lager:error("Plugin ~p is already started", [PluginName]),
|
|
|
+ {error, already_started};
|
|
|
+ false ->
|
|
|
+ case find_plugin(PluginName) of
|
|
|
+ false ->
|
|
|
+ lager:error("Plugin ~s not found", [PluginName]),
|
|
|
+ {error, not_found};
|
|
|
+ Plugin ->
|
|
|
+ load_plugin(Plugin, true)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+load_plugin(#mqtt_plugin{name = Name, config = Config}, Persistent) ->
|
|
|
+ case load_app(Name, Config) of
|
|
|
+ ok ->
|
|
|
+ start_app(Name, fun(App) -> plugin_loaded(App, Persistent) end);
|
|
|
+ {error, Error} ->
|
|
|
+ {error, Error}
|
|
|
+ end.
|
|
|
+
|
|
|
+load_app(App, Config) ->
|
|
|
+ case application:load(App) of
|
|
|
+ ok ->
|
|
|
+ set_config(Config);
|
|
|
+ {error, {already_loaded, App}} ->
|
|
|
+ set_config(Config);
|
|
|
+ {error, Error} ->
|
|
|
+ {error, Error}
|
|
|
+ end.
|
|
|
+
|
|
|
+%% This trick is awesome:)
|
|
|
+set_config([]) ->
|
|
|
+ ok;
|
|
|
+set_config([{AppName, Envs} | Config]) ->
|
|
|
+ [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs],
|
|
|
+ set_config(Config).
|
|
|
+
|
|
|
+start_app(App, SuccFun) ->
|
|
|
+ case application:ensure_all_started(App) of
|
|
|
+ {ok, Started} ->
|
|
|
+ lager:info("Started Apps: ~p, load plugin ~p successfully", [Started, App]),
|
|
|
+ SuccFun(App),
|
|
|
+ {ok, Started};
|
|
|
+ {error, {ErrApp, Reason}} ->
|
|
|
+ lager:error("load plugin ~p error, cannot start app ~s for ~p", [App, ErrApp, Reason]),
|
|
|
+ {error, {ErrApp, Reason}}
|
|
|
+ end.
|
|
|
+
|
|
|
+find_plugin(Name) ->
|
|
|
+ find_plugin(Name, list()).
|
|
|
+
|
|
|
+find_plugin(Name, Plugins) ->
|
|
|
+ lists:keyfind(Name, 2, Plugins).
|
|
|
+
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+%% @doc UnLoad One Plugin
|
|
|
+%% @end
|
|
|
+%%------------------------------------------------------------------------------
|
|
|
+-spec unload(atom()) -> ok | {error, any()}.
|
|
|
+unload(PluginName) when is_atom(PluginName) ->
|
|
|
+ case {lists:member(PluginName, names(started_app)), lists:member(PluginName, names(plugin))} of
|
|
|
+ {true, true} ->
|
|
|
+ unload_plugin(PluginName, true);
|
|
|
+ {false, _} ->
|
|
|
+ lager:error("Plugin ~p is not started", [PluginName]),
|
|
|
+ {error, not_started};
|
|
|
+ {true, false} ->
|
|
|
+ lager:error("~s is not a plugin, cannot unload it", [PluginName]),
|
|
|
+ {error, not_found}
|
|
|
+ end.
|
|
|
+
|
|
|
+unload_plugin(App, Persistent) ->
|
|
|
+ case stop_app(App) of
|
|
|
+ ok ->
|
|
|
+ plugin_unloaded(App, Persistent), ok;
|
|
|
+ {error, Reason} ->
|
|
|
+ {error, Reason}
|
|
|
+ end.
|
|
|
+
|
|
|
+stop_app(App) ->
|
|
|
+ case application:stop(App) of
|
|
|
+ ok ->
|
|
|
+ lager:info("stop plugin ~p successfully~n", [App]), ok;
|
|
|
+ {error, {not_started, App}} ->
|
|
|
+ lager:error("plugin ~p is not started~n", [App]), ok;
|
|
|
+ {error, Reason} ->
|
|
|
+ lager:error("stop plugin ~p error: ~p", [App]), {error, Reason}
|
|
|
+ end.
|
|
|
+
|
|
|
+%%%=============================================================================
|
|
|
+%%% Internal functions
|
|
|
+%%%=============================================================================
|
|
|
+
|
|
|
+names(plugin) ->
|
|
|
+ names(list());
|
|
|
+
|
|
|
+names(started_app) ->
|
|
|
+ [Name || {Name, _Descr, _Ver} <- application:which_applications()];
|
|
|
+
|
|
|
+names(Plugins) ->
|
|
|
+ [Name || #mqtt_plugin{name = Name} <- Plugins].
|
|
|
+
|
|
|
+plugin_loaded(_Name, false) ->
|
|
|
+ ok;
|
|
|
+plugin_loaded(Name, true) ->
|
|
|
+ case read_loaded() of
|
|
|
+ {ok, Names} ->
|
|
|
+ case lists:member(Name, Names) of
|
|
|
+ false ->
|
|
|
+ %% write file if plugin is loaded
|
|
|
+ write_loaded(lists:append(Names, [Name]));
|
|
|
+ true ->
|
|
|
+ ignore
|
|
|
+ end;
|
|
|
+ {error, Error} ->
|
|
|
+ lager:error("Cannot read loaded plugins: ~p", [Error])
|
|
|
+ end.
|
|
|
+
|
|
|
+plugin_unloaded(_Name, false) ->
|
|
|
+ ok;
|
|
|
+plugin_unloaded(Name, true) ->
|
|
|
+ case read_loaded() of
|
|
|
+ {ok, Names} ->
|
|
|
+ case lists:member(Name, Names) of
|
|
|
+ true ->
|
|
|
+ write_loaded(lists:delete(Name, Names));
|
|
|
+ false ->
|
|
|
+ lager:error("Cannot find ~s in loaded_file", [Name])
|
|
|
+ end;
|
|
|
+ {error, Error} ->
|
|
|
+ lager:error("Cannot read loaded_plugins: ~p", [Error])
|
|
|
+ end.
|
|
|
+
|
|
|
+read_loaded() ->
|
|
|
+ {ok, File} = env(loaded_file),
|
|
|
+ read_loaded(File).
|
|
|
+
|
|
|
+read_loaded(File) ->
|
|
|
+ file:consult(File).
|
|
|
+
|
|
|
+write_loaded(AppNames) ->
|
|
|
+ {ok, File} = env(loaded_file),
|
|
|
+ case file:open(File, [binary, write]) of
|
|
|
+ {ok, Fd} ->
|
|
|
+ lists:foreach(fun(Name) ->
|
|
|
+ file:write(Fd, iolist_to_binary(io_lib:format("~s.~n", [Name])))
|
|
|
+ end, AppNames);
|
|
|
+ {error, Error} ->
|
|
|
+ lager:error("Open File ~p Error: ~p", [File, Error]),
|
|
|
+ {error, Error}
|
|
|
+ end.
|
|
|
+
|
|
|
+env(Name) ->
|
|
|
+ case application:get_env(emqttd, plugins) of
|
|
|
+ {ok, PluginsEnv} ->
|
|
|
+ case proplists:get_value(Name, PluginsEnv) of
|
|
|
+ undefined ->
|
|
|
+ undefined;
|
|
|
+ Val ->
|
|
|
+ {ok, Val}
|
|
|
+ end;
|
|
|
+ undefined ->
|
|
|
+ undefined
|
|
|
+ end.
|
|
|
+
|