diff options
Diffstat (limited to 'src/gen_mod.erl')
-rw-r--r-- | src/gen_mod.erl | 766 |
1 files changed, 457 insertions, 309 deletions
diff --git a/src/gen_mod.erl b/src/gen_mod.erl index c4306577c..58ec9950f 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -5,7 +5,7 @@ %%% Created : 24 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2016 ProcessOne +%%% ejabberd, Copyright (C) 2002-2019 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -22,156 +22,235 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- - -module(gen_mod). - --behaviour(ejabberd_config). - +-behaviour(supervisor). -author('alexey@process-one.net'). --export([start/0, start_module/2, start_module/3, - stop_module/2, stop_module_keep_config/2, get_opt/3, - get_opt/4, get_opt_host/3, db_type/2, db_type/3, - get_module_opt/4, get_module_opt/5, get_module_opt_host/3, +-export([init/1, start_link/0, start_child/3, start_child/4, + stop_child/1, stop_child/2, stop/0, config_reloaded/0]). +-export([start_module/2, stop_module/2, stop_module_keep_config/2, + get_opt/2, set_opt/3, get_opt_hosts/1, is_equal_opt/3, + get_module_opt/3, get_module_opts/2, get_module_opt_hosts/2, loaded_modules/1, loaded_modules_with_opts/1, - get_hosts/2, get_module_proc/2, is_loaded/2, + get_hosts/2, get_module_proc/2, is_loaded/2, is_loaded_elsewhere/2, start_modules/0, start_modules/1, stop_modules/0, stop_modules/1, - opt_type/1, db_mod/2, db_mod/3]). + db_mod/2, ram_db_mod/2]). +-export([validate/2]). -%%-export([behaviour_info/1]). +%% Deprecated functions +%% update_module/3 is used by test suite ONLY +-export([update_module/3]). +-deprecated([{update_module, 3}]). --include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-include("ejabberd_stacktrace.hrl"). -record(ejabberd_module, {module_host = {undefined, <<"">>} :: {atom(), binary()}, - opts = [] :: opts() | '_' | '$2'}). + opts = [] :: opts() | '_' | '$2', + order = 0 :: integer()}). --type opts() :: [{atom(), any()}]. --type db_type() :: sql | mnesia | riak. +-type opts() :: #{atom() => term()}. +-type db_type() :: atom(). --callback start(binary(), opts()) -> any(). +-callback start(binary(), opts()) -> ok | {ok, pid()} | {error, term()}. -callback stop(binary()) -> any(). --callback mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. +-callback reload(binary(), opts(), opts()) -> ok | {ok, pid()} | {error, term()}. +-callback mod_opt_type(atom()) -> econf:validator(). +-callback mod_options(binary()) -> [{atom(), term()} | atom()]. -callback depends(binary(), opts()) -> [{module(), hard | soft}]. +-optional_callbacks([mod_opt_type/1, reload/3]). + -export_type([opts/0]). -export_type([db_type/0]). -%%behaviour_info(callbacks) -> [{start, 2}, {stop, 1}]; -%%behaviour_info(_Other) -> undefined. +-ifndef(GEN_SERVER). +-define(GEN_SERVER, gen_server). +-endif. + +start_link() -> + case supervisor:start_link({local, ejabberd_gen_mod_sup}, ?MODULE, []) of + {ok, Pid} -> + start_modules(), + {ok, Pid}; + Err -> + Err + end. -start() -> +init([]) -> + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), + ejabberd_hooks:add(host_up, ?MODULE, start_modules, 40), + ejabberd_hooks:add(host_down, ?MODULE, stop_modules, 70), ets:new(ejabberd_modules, [named_table, public, - {keypos, #ejabberd_module.module_host}]), - ok. + {keypos, #ejabberd_module.module_host}, + {read_concurrency, true}]), + {ok, {{one_for_one, 10, 1}, []}}. + +-spec stop() -> ok. +stop() -> + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50), + ejabberd_hooks:delete(host_up, ?MODULE, start_modules, 40), + ejabberd_hooks:delete(host_down, ?MODULE, stop_modules, 70), + stop_modules(), + ejabberd_sup:stop_child(ejabberd_gen_mod_sup). + +-spec start_child(module(), binary(), opts()) -> {ok, pid()} | {error, any()}. +start_child(Mod, Host, Opts) -> + start_child(Mod, Host, Opts, get_module_proc(Host, Mod)). + +-spec start_child(module(), binary(), opts(), atom()) -> {ok, pid()} | {error, any()}. +start_child(Mod, Host, Opts, Proc) -> + Spec = {Proc, {?GEN_SERVER, start_link, + [{local, Proc}, Mod, [Host, Opts], + ejabberd_config:fsm_limit_opts([])]}, + transient, timer:minutes(1), worker, [Mod]}, + supervisor:start_child(ejabberd_gen_mod_sup, Spec). + +-spec stop_child(module(), binary()) -> ok | {error, any()}. +stop_child(Mod, Host) -> + stop_child(get_module_proc(Host, Mod)). + +-spec stop_child(atom()) -> ok | {error, any()}. +stop_child(Proc) -> + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc). -spec start_modules() -> any(). - -%% Start all the modules in all the hosts start_modules() -> - lists:foreach( - fun(Host) -> - start_modules(Host) - end, ?MYHOSTS). - -get_modules_options(Host) -> - ejabberd_config:get_option( - {modules, Host}, - fun(Mods) -> - lists:map( - fun({M, A}) when is_atom(M), is_list(A) -> - {M, A} - end, Mods) - end, []). - -sort_modules(Host, ModOpts) -> - G = digraph:new([acyclic]), - lists:foreach( - fun({Mod, Opts}) -> - digraph:add_vertex(G, Mod, Opts), - Deps = try Mod:depends(Host, Opts) catch _:undef -> [] end, - lists:foreach( - fun({DepMod, Type}) -> - case lists:keyfind(DepMod, 1, ModOpts) of - false when Type == hard -> - ErrTxt = io_lib:format( - "failed to load module '~s' " - "because it depends on module '~s' " - "which is not found in the config", - [Mod, DepMod]), - ?ERROR_MSG(ErrTxt, []), - digraph:del_vertex(G, Mod), - maybe_halt_ejabberd(ErrTxt); - false when Type == soft -> - ?WARNING_MSG("module '~s' is recommended for " - "module '~s' but is not found in " - "the config", - [DepMod, Mod]); - {DepMod, DepOpts} -> - digraph:add_vertex(G, DepMod, DepOpts), - case digraph:add_edge(G, DepMod, Mod) of - {error, {bad_edge, Path}} -> - ?WARNING_MSG("cyclic dependency detected " - "between modules: ~p", - [Path]); - _ -> - ok - end - end - end, Deps) - end, ModOpts), - [digraph:vertex(G, V) || V <- digraph_utils:topsort(G)]. - --spec start_modules(binary()) -> any(). + Hosts = ejabberd_option:hosts(), + ?INFO_MSG("Loading modules for ~ts", [misc:format_hosts_list(Hosts)]), + lists:foreach(fun start_modules/1, Hosts). +-spec start_modules(binary()) -> ok. start_modules(Host) -> - Modules = sort_modules(Host, get_modules_options(Host)), + Modules = ejabberd_option:modules(Host), lists:foreach( - fun({Module, Opts}) -> - start_module(Host, Module, Opts) + fun({Module, Opts, Order}) -> + start_module(Host, Module, Opts, Order) end, Modules). --spec start_module(binary(), atom()) -> any(). - +-spec start_module(binary(), atom()) -> ok | {ok, pid()} | {error, not_found_in_config}. start_module(Host, Module) -> - Modules = get_modules_options(Host), + Modules = ejabberd_option:modules(Host), case lists:keyfind(Module, 1, Modules) of - {_, Opts} -> - start_module(Host, Module, Opts); + {_, Opts, Order} -> + start_module(Host, Module, Opts, Order); false -> {error, not_found_in_config} end. --spec start_module(binary(), atom(), opts()) -> any(). +-spec start_module(binary(), atom(), opts(), integer()) -> ok | {ok, pid()}. +start_module(Host, Module, Opts, Order) -> + ?DEBUG("Loading ~ts at ~ts", [Module, Host]), + store_options(Host, Module, Opts, Order), + try case Module:start(Host, Opts) of + ok -> ok; + {ok, Pid} when is_pid(Pid) -> {ok, Pid}; + Err -> + ets:delete(ejabberd_modules, {Module, Host}), + erlang:error({bad_return, Module, Err}) + end + catch ?EX_RULE(Class, Reason, Stack) -> + StackTrace = ?EX_STACK(Stack), + ets:delete(ejabberd_modules, {Module, Host}), + ErrorText = format_module_error( + Module, start, 2, + Opts, Class, Reason, + StackTrace), + ?CRITICAL_MSG(ErrorText, []), + maybe_halt_ejabberd(), + erlang:raise(Class, Reason, StackTrace) + end. + +-spec reload_modules(binary()) -> ok. +reload_modules(Host) -> + NewMods = ejabberd_option:modules(Host), + OldMods = lists:reverse(loaded_modules_with_opts(Host)), + lists:foreach( + fun({Mod, _Opts}) -> + case lists:keymember(Mod, 1, NewMods) of + false -> + stop_module(Host, Mod); + true -> + ok + end + end, OldMods), + lists:foreach( + fun({Mod, Opts, Order}) -> + case lists:keymember(Mod, 1, OldMods) of + false -> + start_module(Host, Mod, Opts, Order); + true -> + ok + end + end, NewMods), + lists:foreach( + fun({Mod, OldOpts}) -> + case lists:keyfind(Mod, 1, NewMods) of + {_, NewOpts, Order} -> + if OldOpts /= NewOpts -> + reload_module(Host, Mod, NewOpts, OldOpts, Order); + true -> + ok + end; + _ -> + ok + end + end, OldMods). + +-spec reload_module(binary(), module(), opts(), opts(), integer()) -> ok | {ok, pid()}. +reload_module(Host, Module, NewOpts, OldOpts, Order) -> + case erlang:function_exported(Module, reload, 3) of + true -> + ?DEBUG("Reloading ~ts at ~ts", [Module, Host]), + store_options(Host, Module, NewOpts, Order), + try case Module:reload(Host, NewOpts, OldOpts) of + ok -> ok; + {ok, Pid} when is_pid(Pid) -> {ok, Pid}; + Err -> erlang:error({bad_return, Module, Err}) + end + catch ?EX_RULE(Class, Reason, Stack) -> + StackTrace = ?EX_STACK(Stack), + ErrorText = format_module_error( + Module, reload, 3, + NewOpts, Class, Reason, + StackTrace), + ?CRITICAL_MSG(ErrorText, []), + erlang:raise(Class, Reason, StackTrace) + end; + false -> + ?WARNING_MSG("Module ~ts doesn't support reloading " + "and will be restarted", [Module]), + stop_module(Host, Module), + start_module(Host, Module, NewOpts, Order) + end. + +-spec update_module(binary(), module(), opts()) -> ok | {ok, pid()}. +update_module(Host, Module, Opts) -> + case ets:lookup(ejabberd_modules, {Module, Host}) of + [#ejabberd_module{opts = OldOpts, order = Order}] -> + NewOpts = maps:merge(OldOpts, Opts), + reload_module(Host, Module, NewOpts, OldOpts, Order); + [] -> + erlang:error({module_not_loaded, Module, Host}) + end. -start_module(Host, Module, Opts0) -> - Opts = validate_opts(Module, Opts0), +-spec store_options(binary(), module(), opts(), integer()) -> true. +store_options(Host, Module, Opts, Order) -> ets:insert(ejabberd_modules, #ejabberd_module{module_host = {Module, Host}, - opts = Opts}), - try Module:start(Host, Opts) catch - Class:Reason -> - ets:delete(ejabberd_modules, {Module, Host}), - ErrorText = - io_lib:format("Problem starting the module ~p for host " - "~p ~n options: ~p~n ~p: ~p~n~p", - [Module, Host, Opts, Class, Reason, - erlang:get_stacktrace()]), - ?CRITICAL_MSG(ErrorText, []), - maybe_halt_ejabberd(ErrorText), - erlang:raise(Class, Reason, erlang:get_stacktrace()) - end. + opts = Opts, order = Order}). -maybe_halt_ejabberd(ErrorText) -> +maybe_halt_ejabberd() -> case is_app_running(ejabberd) of false -> ?CRITICAL_MSG("ejabberd initialization was aborted " "because a module start failed.", []), - timer:sleep(3000), - erlang:halt(string:substr(lists:flatten(ErrorText), 1, 199)); + ejabberd:halt(); true -> ok end. @@ -181,25 +260,22 @@ is_app_running(AppName) -> lists:keymember(AppName, 1, application:which_applications(Timeout)). --spec stop_modules() -> any(). - +-spec stop_modules() -> ok. stop_modules() -> lists:foreach( - fun(Host) -> - stop_modules(Host) - end, ?MYHOSTS). - --spec stop_modules(binary()) -> any(). + fun(Host) -> + stop_modules(Host) + end, ejabberd_option:hosts()). +-spec stop_modules(binary()) -> ok. stop_modules(Host) -> - Modules = get_modules_options(Host), + Modules = lists:reverse(loaded_modules_with_opts(Host)), lists:foreach( fun({Module, _Args}) -> - gen_mod:stop_module_keep_config(Host, Module) + stop_module_keep_config(Host, Module) end, Modules). -spec stop_module(binary(), atom()) -> error | {aborted, any()} | {atomic, any()}. - stop_module(Host, Module) -> case stop_module_keep_config(Host, Module) of error -> error; @@ -207,211 +283,101 @@ stop_module(Host, Module) -> end. -spec stop_module_keep_config(binary(), atom()) -> error | ok. - stop_module_keep_config(Host, Module) -> - case catch Module:stop(Host) of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), error; - {wait, ProcList} when is_list(ProcList) -> - lists:foreach(fun wait_for_process/1, ProcList), - ets:delete(ejabberd_modules, {Module, Host}), - ok; - {wait, Process} -> - wait_for_process(Process), - ets:delete(ejabberd_modules, {Module, Host}), - ok; - _ -> ets:delete(ejabberd_modules, {Module, Host}), ok - end. - -wait_for_process(Process) -> - MonitorReference = erlang:monitor(process, Process), - wait_for_stop(Process, MonitorReference). - -wait_for_stop(Process, MonitorReference) -> - receive - {'DOWN', MonitorReference, _Type, _Object, _Info} -> ok - after 5000 -> - catch exit(whereis(Process), kill), - wait_for_stop1(MonitorReference) - end. - -wait_for_stop1(MonitorReference) -> - receive - {'DOWN', MonitorReference, _Type, _Object, _Info} -> ok - after 5000 -> ok - end. - --type check_fun() :: fun((any()) -> any()) | {module(), atom()}. - --spec get_opt(atom() | {atom(), binary()|global}, opts(), check_fun()) -> any(). - -get_opt(Opt, Opts, F) -> - get_opt(Opt, Opts, F, undefined). - --spec get_opt(atom() | {atom(), binary()|global}, opts(), check_fun(), any()) -> any(). - -get_opt({Opt, Host}, Opts, F, Default) -> - case lists:keysearch(Opt, 1, Opts) of - false -> - ejabberd_config:get_option({Opt, Host}, F, Default); - {value, {_, Val}} -> - ejabberd_config:prepare_opt_val(Opt, Val, F, Default) - end; -get_opt(Opt, Opts, F, Default) -> - case lists:keysearch(Opt, 1, Opts) of - false -> - Default; - {value, {_, Val}} -> - ejabberd_config:prepare_opt_val(Opt, Val, F, Default) - end. - --spec get_module_opt(global | binary(), atom(), atom(), check_fun()) -> any(). - -get_module_opt(Host, Module, Opt, F) -> - get_module_opt(Host, Module, Opt, F, undefined). - --spec get_module_opt(global | binary(), atom(), atom(), check_fun(), any()) -> any(). - -get_module_opt(global, Module, Opt, F, Default) -> - Hosts = (?MYHOSTS), - [Value | Values] = lists:map(fun (Host) -> - get_module_opt(Host, Module, Opt, - F, Default) - end, - Hosts), - Same_all = lists:all(fun (Other_value) -> - Other_value == Value - end, - Values), - case Same_all of - true -> Value; - false -> Default - end; -get_module_opt(Host, Module, Opt, F, Default) -> - OptsList = ets:lookup(ejabberd_modules, {Module, Host}), - case OptsList of - [] -> Default; - [#ejabberd_module{opts = Opts} | _] -> - get_opt(Opt, Opts, F, Default) - end. - --spec get_module_opt_host(global | binary(), atom(), binary()) -> binary(). - -get_module_opt_host(Host, Module, Default) -> - Val = get_module_opt(Host, Module, host, - fun iolist_to_binary/1, - Default), - ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). - --spec get_opt_host(binary(), opts(), binary()) -> binary(). - -get_opt_host(Host, Opts, Default) -> - Val = get_opt(host, Opts, fun iolist_to_binary/1, Default), - ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). - -validate_opts(Module, Opts) -> - lists:filtermap( - fun({Opt, Val}) -> - case catch Module:mod_opt_type(Opt) of - VFun when is_function(VFun) -> - try VFun(Val) of - _ -> - true - catch {replace_with, NewVal} -> - {true, {Opt, NewVal}}; - {invalid_syntax, Error} -> - ?ERROR_MSG("ignoring invalid value '~p' for " - "option '~s' of module '~s': ~s", - [Val, Opt, Module, Error]), - false; - _:_ -> - ?ERROR_MSG("ignoring invalid value '~p' for " - "option '~s' of module '~s'", - [Val, Opt, Module]), - false - end; - L when is_list(L) -> - SOpts = str:join([[$', atom_to_list(A), $'] || A <- L], <<", ">>), - ?ERROR_MSG("unknown option '~s' for module '~s' will be" - " likely ignored, available options are: ~s", - [Opt, Module, SOpts]), - true; - {'EXIT', {undef, _}} -> - ?WARNING_MSG("module '~s' doesn't export mod_opt_type/1", - [Module]), - true - end; - (Junk) -> - ?ERROR_MSG("failed to understand option ~p for module '~s'", - [Junk, Module]), - false - end, Opts). - --spec db_type(binary() | global, module()) -> db_type(); - (opts(), module()) -> db_type(). - -db_type(Opts, Module) when is_list(Opts) -> - db_type(global, Opts, Module); -db_type(Host, Module) when is_atom(Module) -> - case catch Module:mod_opt_type(db_type) of - F when is_function(F) -> - case get_module_opt(Host, Module, db_type, F) of - undefined -> ejabberd_config:default_db(Host, Module); - Type -> Type - end; + ?DEBUG("Stopping ~ts at ~ts", [Module, Host]), + try Module:stop(Host) of _ -> - undefined + ets:delete(ejabberd_modules, {Module, Host}), + ok + catch ?EX_RULE(Class, Reason, St) -> + StackTrace = ?EX_STACK(St), + ?ERROR_MSG("Failed to stop module ~ts at ~ts:~n** ~ts", + [Module, Host, + misc:format_exception(2, Class, Reason, StackTrace)]), + error end. --spec db_type(binary(), opts(), module()) -> db_type(). - -db_type(Host, Opts, Module) -> - case catch Module:mod_opt_type(db_type) of - F when is_function(F) -> - case get_opt(db_type, Opts, F) of - undefined -> ejabberd_config:default_db(Host, Module); - Type -> Type - end; - _ -> - undefined +-spec get_opt(atom(), opts()) -> any(). +get_opt(Opt, Opts) -> + maps:get(Opt, Opts). + +-spec set_opt(atom(), term(), opts()) -> opts(). +set_opt(Opt, Val, Opts) -> + maps:put(Opt, Val, Opts). + +-spec get_module_opt(global | binary(), atom(), atom()) -> any(). +get_module_opt(global, Module, Opt) -> + get_module_opt(ejabberd_config:get_myname(), Module, Opt); +get_module_opt(Host, Module, Opt) -> + Opts = get_module_opts(Host, Module), + get_opt(Opt, Opts). + +-spec get_module_opt_hosts(binary(), module()) -> [binary()]. +get_module_opt_hosts(Host, Module) -> + Opts = get_module_opts(Host, Module), + get_opt_hosts(Opts). + +-spec get_opt_hosts(opts()) -> [binary()]. +get_opt_hosts(Opts) -> + case get_opt(hosts, Opts) of + L when L == [] orelse L == undefined -> + [get_opt(host, Opts)]; + L -> + L end. --spec db_mod(binary() | global | db_type(), module()) -> module(). +-spec get_module_opts(binary(), module()) -> opts(). +get_module_opts(Host, Module) -> + try ets:lookup_element(ejabberd_modules, {Module, Host}, 3) + catch _:badarg -> erlang:error({module_not_loaded, Module, Host}) + end. -db_mod(Type, Module) when is_atom(Type) -> - list_to_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(Type)); -db_mod(Host, Module) when is_binary(Host) orelse Host == global -> - db_mod(db_type(Host, Module), Module). +-spec db_mod(binary() | global | db_type() | opts(), module()) -> module(). +db_mod(T, M) -> + db_mod(db_type, T, M). --spec db_mod(binary() | global, opts(), module()) -> module(). +-spec ram_db_mod(binary() | global | db_type() | opts(), module()) -> module(). +ram_db_mod(T, M) -> + db_mod(ram_db_type, T, M). -db_mod(Host, Opts, Module) when is_list(Opts) -> - db_mod(db_type(Host, Opts, Module), Module). +-spec db_mod(db_type | ram_db_type, + binary() | global | db_type() | opts(), module()) -> module(). +db_mod(Opt, Host, Module) when is_binary(Host) orelse Host == global -> + db_mod(Opt, get_module_opt(Host, Module, Opt), Module); +db_mod(Opt, Opts, Module) when is_map(Opts) -> + db_mod(Opt, get_opt(Opt, Opts), Module); +db_mod(_Opt, Type, Module) when is_atom(Type) -> + list_to_existing_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(Type)). -spec loaded_modules(binary()) -> [atom()]. - loaded_modules(Host) -> - ets:select(ejabberd_modules, - [{#ejabberd_module{_ = '_', module_host = {'$1', Host}}, - [], ['$1']}]). + Mods = ets:select( + ejabberd_modules, + ets:fun2ms( + fun(#ejabberd_module{module_host = {Mod, H}, + order = Order}) when H == Host -> + {Mod, Order} + end)), + [Mod || {Mod, _} <- lists:keysort(2, Mods)]. -spec loaded_modules_with_opts(binary()) -> [{atom(), opts()}]. - loaded_modules_with_opts(Host) -> - ets:select(ejabberd_modules, - [{#ejabberd_module{_ = '_', module_host = {'$1', Host}, - opts = '$2'}, - [], [{{'$1', '$2'}}]}]). + Mods = ets:select( + ejabberd_modules, + ets:fun2ms( + fun(#ejabberd_module{module_host = {Mod, H}, opts = Opts, + order = Order}) when H == Host -> + {Mod, Opts, Order} + end)), + [{Mod, Opts} || {Mod, Opts, _} <- lists:keysort(3, Mods)]. -spec get_hosts(opts(), binary()) -> [binary()]. - get_hosts(Opts, Prefix) -> - case get_opt(hosts, Opts, - fun(Hs) -> [iolist_to_binary(H) || H <- Hs] end) of + case get_opt(hosts, Opts) of undefined -> - case get_opt(host, Opts, - fun iolist_to_binary/1) of + case get_opt(host, Opts) of undefined -> - [<<Prefix/binary, Host/binary>> || Host <- ?MYHOSTS]; + [<<Prefix/binary, Host/binary>> || Host <- ejabberd_option:hosts()]; Host -> [Host] end; @@ -419,20 +385,202 @@ get_hosts(Opts, Prefix) -> Hosts end. --spec get_module_proc(binary(), {frontend, atom()} | atom()) -> atom(). - -get_module_proc(Host, {frontend, Base}) -> - get_module_proc(<<"frontend_", Host/binary>>, Base); +-spec get_module_proc(binary() | global, atom()) -> atom(). +get_module_proc(global, Base) -> + get_module_proc(<<"global">>, Base); get_module_proc(Host, Base) -> binary_to_atom( <<(erlang:atom_to_binary(Base, latin1))/binary, "_", Host/binary>>, latin1). -spec is_loaded(binary(), atom()) -> boolean(). - is_loaded(Host, Module) -> ets:member(ejabberd_modules, {Module, Host}). -opt_type(default_db) -> fun(T) when is_atom(T) -> T end; -opt_type(modules) -> fun (L) when is_list(L) -> L end; -opt_type(_) -> [default_db, modules]. +-spec is_loaded_elsewhere(binary(), atom()) -> boolean(). +is_loaded_elsewhere(Host, Module) -> + ets:select_count( + ejabberd_modules, + ets:fun2ms( + fun(#ejabberd_module{module_host = {Mod, H}}) -> + (Mod == Module) and (H /= Host) + end)) /= 0. + +-spec config_reloaded() -> ok. +config_reloaded() -> + lists:foreach(fun reload_modules/1, ejabberd_option:hosts()). + +-spec is_equal_opt(atom(), opts(), opts()) -> true | {false, any(), any()}. +is_equal_opt(Opt, NewOpts, OldOpts) -> + NewVal = get_opt(Opt, NewOpts), + OldVal = get_opt(Opt, OldOpts), + if NewVal /= OldVal -> + {false, NewVal, OldVal}; + true -> + true + end. + +%%%=================================================================== +%%% Formatters +%%%=================================================================== +-spec format_module_error(atom(), start | reload, non_neg_integer(), opts(), + error | exit | throw, any(), + [erlang:stack_item()]) -> iolist(). +format_module_error(Module, Fun, Arity, Opts, Class, Reason, St) -> + case {Class, Reason} of + {error, {bad_return, Module, {error, _} = Err}} -> + io_lib:format("Failed to ~ts module ~ts: ~ts", + [Fun, Module, misc:format_val(Err)]); + {error, {bad_return, Module, Ret}} -> + io_lib:format("Module ~ts returned unexpected value from ~ts/~B:~n" + "** Error: ~p~n" + "** Hint: this is either not an ejabberd module " + "or it implements ejabberd API incorrectly", + [Module, Fun, Arity, Ret]); + _ -> + io_lib:format("Internal error of module ~ts has " + "occurred during ~ts:~n" + "** Options: ~p~n" + "** ~ts", + [Module, Fun, Opts, + misc:format_exception(2, Class, Reason, St)]) + end. + +%%%=================================================================== +%%% Validation +%%%=================================================================== +-spec validator(binary()) -> econf:validator(). +validator(Host) -> + econf:options( + #{modules => + econf:and_then( + econf:map( + econf:beam([{start, 2}, {stop, 1}, + {mod_options, 1}, {depends, 2}]), + econf:options( + #{db_type => econf:atom(), + ram_db_type => econf:atom(), + '_' => econf:any()})), + fun(L) -> + Validators = maps:from_list( + lists:map( + fun({Mod, Opts}) -> + {Mod, validator(Host, Mod, Opts)} + end, L)), + Validator = econf:options(Validators, [unique]), + Validator(L) + end)}). + +-spec validator(binary(), module(), [{atom(), term()}]) -> econf:validator(). +validator(Host, Module, Opts) -> + {Required, {DefaultOpts1, Validators}} = + lists:mapfoldl( + fun({M, DefOpts}, {DAcc, VAcc}) -> + lists:mapfoldl( + fun({Opt, Def}, {DAcc1, VAcc1}) -> + {[], {DAcc1#{Opt => Def}, + VAcc1#{Opt => get_opt_type(Module, M, Opt)}}}; + (Opt, {DAcc1, VAcc1}) -> + {[Opt], {DAcc1, + VAcc1#{Opt => get_opt_type(Module, M, Opt)}}} + end, {DAcc, VAcc}, DefOpts) + end, {#{}, #{}}, get_defaults(Host, Module, Opts)), + econf:and_then( + econf:options( + Validators, + [{required, lists:usort(lists:flatten(Required))}, + {return, map}, unique]), + fun(Opts1) -> + maps:merge(DefaultOpts1, Opts1) + end). + +-spec validate(binary(), [{module(), opts()}]) -> + {ok, [{module(), opts(), integer()}]} | + econf:error_return(). +validate(Host, ModOpts) -> + case econf:validate(validator(Host), [{modules, ModOpts}]) of + {ok, [{modules, ModOpts1}]} -> + try sort_modules(Host, ModOpts1) + catch throw:{?MODULE, Reason} -> + {error, Reason, [modules]} + end; + {error, _, _} = Err -> + Err + end. + +-spec get_defaults(binary(), module(), [{atom(), term()}]) -> + [{module(), [{atom(), term()} | atom()]}]. +get_defaults(Host, Module, Opts) -> + DefaultOpts = Module:mod_options(Host), + [{Module, DefaultOpts}| + lists:filtermap( + fun({Opt, T1}) when Opt == db_type; Opt == ram_db_type -> + T2 = proplists:get_value(Opt, Opts, T1), + DBMod = list_to_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(T2)), + case code:ensure_loaded(DBMod) of + {module, _} -> + case erlang:function_exported(DBMod, mod_options, 1) of + true -> + {true, {DBMod, DBMod:mod_options(Host)}}; + false -> + false + end; + _ -> + false + end; + (_) -> + false + end, DefaultOpts)]. + +-spec get_opt_type(module(), module(), atom()) -> econf:validator(). +get_opt_type(Mod, SubMod, Opt) -> + try SubMod:mod_opt_type(Opt) + catch _:_ -> Mod:mod_opt_type(Opt) + end. + +-spec sort_modules(binary(), [{module(), opts()}]) -> {ok, [{module(), opts(), integer()}]}. +sort_modules(Host, ModOpts) -> + G = digraph:new([acyclic]), + lists:foreach( + fun({Mod, Opts}) -> + digraph:add_vertex(G, Mod, Opts), + Deps = Mod:depends(Host, Opts), + lists:foreach( + fun({DepMod, Type}) -> + case lists:keyfind(DepMod, 1, ModOpts) of + false when Type == hard -> + throw({?MODULE, {missing_module_dep, Mod, DepMod}}); + false when Type == soft -> + warn_soft_dep_fail(DepMod, Mod); + {DepMod, DepOpts} -> + digraph:add_vertex(G, DepMod, DepOpts), + case digraph:add_edge(G, DepMod, Mod) of + {error, {bad_edge, Path}} -> + warn_cyclic_dep(Path); + _ -> + ok + end + end + end, Deps) + end, ModOpts), + {Result, _} = lists:mapfoldl( + fun(V, Order) -> + {M, O} = digraph:vertex(G, V), + {{M, O, Order}, Order+1} + end, 1, digraph_utils:topsort(G)), + digraph:delete(G), + {ok, Result}. + +-spec warn_soft_dep_fail(module(), module()) -> ok. +warn_soft_dep_fail(DepMod, Mod) -> + ?WARNING_MSG("Module ~ts is recommended for module " + "~ts but is not found in the config", + [DepMod, Mod]). + +-spec warn_cyclic_dep([module()]) -> ok. +warn_cyclic_dep(Path) -> + ?WARNING_MSG("Cyclic dependency detected between modules ~ts. " + "This is either a bug, or the modules are not " + "supposed to work together in this configuration. " + "The modules will still be loaded though", + [misc:format_cycle(Path)]). |