1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : gen_mod.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : |
5 |
|
%%% Created : 24 Jan 2003 by Alexey Shchepin <alexey@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
-module(gen_mod). |
27 |
|
-author('alexey@process-one.net'). |
28 |
|
|
29 |
|
-type dep_arguments() :: proplists:proplist(). |
30 |
|
-type deps_list() :: [ |
31 |
|
{module(), dep_arguments(), gen_mod_deps:hardness()} | |
32 |
|
{module(), gen_mod_deps:hardness()} | |
33 |
|
{service, mongoose_service:service()} |
34 |
|
]. |
35 |
|
|
36 |
|
-type module_deps_list() :: [ |
37 |
|
{module(), dep_arguments(), gen_mod_deps:hardness()} | |
38 |
|
{module(), gen_mod_deps:hardness()} |
39 |
|
]. |
40 |
|
|
41 |
|
-type service_deps_list() :: [atom()]. |
42 |
|
|
43 |
|
-export_type([deps_list/0, |
44 |
|
module_opts/0]). |
45 |
|
|
46 |
|
-export([ |
47 |
|
% Modules start & stop, do NOT use in the tests, use mongoose_modules API instead |
48 |
|
start_module/3, |
49 |
|
start_backend_module/2, |
50 |
|
start_backend_module/3, |
51 |
|
stop_module/2, |
52 |
|
does_module_support/2, |
53 |
|
config_spec/1, |
54 |
|
% Get/set opts by host or from a list |
55 |
|
get_opt/2, |
56 |
|
get_opt/3, |
57 |
|
get_opt/4, |
58 |
|
set_opt/3, |
59 |
|
get_module_opt/4, |
60 |
|
get_module_opts/2, |
61 |
|
get_loaded_module_opts/2, |
62 |
|
get_opt_subhost/3, |
63 |
|
get_module_opt_subhost/3, |
64 |
|
|
65 |
|
loaded_modules/0, |
66 |
|
loaded_modules/1, |
67 |
|
loaded_modules_with_opts/0, |
68 |
|
loaded_modules_with_opts/1, |
69 |
|
hosts_with_module/1, |
70 |
|
hosts_and_opts_with_module/1, |
71 |
|
get_module_proc/2, |
72 |
|
is_loaded/2, |
73 |
|
get_deps/3]). |
74 |
|
|
75 |
|
-export([is_app_running/1]). % we have to mock it in some tests |
76 |
|
|
77 |
|
-ignore_xref([behaviour_info/1, loaded_modules_with_opts/0, |
78 |
|
loaded_modules_with_opts/1, hosts_and_opts_with_module/1]). |
79 |
|
|
80 |
|
-include("mongoose.hrl"). |
81 |
|
|
82 |
|
-type module_feature() :: atom(). |
83 |
|
-type domain_name() :: mongooseim:domain_name(). |
84 |
|
-type host_type() :: mongooseim:host_type(). |
85 |
|
-type module_opts() :: list(). |
86 |
|
|
87 |
|
%% -export([behaviour_info/1]). |
88 |
|
%% behaviour_info(callbacks) -> |
89 |
|
%% [{start, 2}, |
90 |
|
%% {stop, 1}]; |
91 |
|
%% behaviour_info(_Other) -> |
92 |
|
%% undefined. |
93 |
|
-callback start(HostType :: host_type(), Opts :: module_opts()) -> any(). |
94 |
|
-callback stop(HostType :: host_type()) -> any(). |
95 |
|
-callback supported_features() -> [module_feature()]. |
96 |
|
-callback config_spec() -> mongoose_config_spec:config_section(). |
97 |
|
|
98 |
|
%% Optional callback specifying module dependencies. |
99 |
|
%% The dependent module can specify parameters with which the dependee should be |
100 |
|
%% started (the parameters will be merged with params given in user config and |
101 |
|
%% by other modules). |
102 |
|
%% The last element of the tuple specifies whether the ordering can be broken in |
103 |
|
%% case of cycle (in that case soft dependency may be started after the |
104 |
|
%% dependent module). |
105 |
|
%% |
106 |
|
%% TODO: think about getting rid of HostType param for deps/2 interface, currently |
107 |
|
%% it's used only by global_distrib modules (see mod_global_distrib_utils:deps/4 |
108 |
|
%% function). |
109 |
|
-callback deps(HostType :: host_type(), Opts :: proplists:list()) -> deps_list(). |
110 |
|
|
111 |
|
-optional_callbacks([config_spec/0, supported_features/0, deps/2]). |
112 |
|
|
113 |
|
%% @doc This function should be called by mongoose_modules only. |
114 |
|
%% To start a new module at runtime, use mongoose_modules:ensure_module/3 instead. |
115 |
|
-spec start_module(HostType :: host_type(), |
116 |
|
Module :: module(), |
117 |
|
Opts :: [any()]) -> {ok, term()}. |
118 |
|
start_module(HostType, Module, Opts) -> |
119 |
4476 |
assert_loaded(HostType, Module), |
120 |
4476 |
start_module_for_host_type(HostType, Module, Opts). |
121 |
|
|
122 |
|
start_module_for_host_type(HostType, Module, Opts) -> |
123 |
4476 |
{links, LinksBefore} = erlang:process_info(self(), links), |
124 |
4476 |
try |
125 |
4476 |
lists:map(fun mongoose_service:assert_loaded/1, |
126 |
|
get_required_services(HostType, Module, Opts)), |
127 |
4476 |
check_dynamic_domains_support(HostType, Module), |
128 |
4476 |
Res = Module:start(HostType, Opts), |
129 |
4475 |
{links, LinksAfter} = erlang:process_info(self(), links), |
130 |
4475 |
case lists:sort(LinksBefore) =:= lists:sort(LinksAfter) of |
131 |
4475 |
true -> ok; |
132 |
|
false -> |
133 |
|
%% TODO: grepping for "fail_ci_build=true" is bad option |
134 |
|
%% for ci testing, rework this. |
135 |
:-( |
CIInfo = "fail_ci_build=true ", |
136 |
|
%% Note for programmers: |
137 |
|
%% Never call start_link directly from your_module:start/2 function! |
138 |
|
%% The process will be killed if we start modules remotely or in shell |
139 |
:-( |
?LOG_ERROR(#{what => unexpected_links, ci_info => CIInfo, |
140 |
:-( |
links_before => LinksBefore, links_after => LinksAfter}) |
141 |
|
end, |
142 |
4475 |
?LOG_DEBUG(#{what => module_started, module => Module, host_type => HostType}), |
143 |
|
% normalise result |
144 |
4475 |
case Res of |
145 |
390 |
{ok, R} -> {ok, R}; |
146 |
4085 |
_ -> {ok, Res} |
147 |
|
end |
148 |
|
catch |
149 |
|
Class:Reason:StackTrace -> |
150 |
1 |
ErrorText = io_lib:format("Problem starting the module ~p for " |
151 |
|
"host_type ~p~n options: ~p~n ~p: ~p~n~p", |
152 |
|
[Module, HostType, Opts, Class, Reason, |
153 |
|
StackTrace]), |
154 |
1 |
?LOG_CRITICAL(#{what => module_start_failed, module => Module, |
155 |
|
host_type => HostType, opts => Opts, class => Class, |
156 |
:-( |
reason => Reason, stacktrace => StackTrace}), |
157 |
1 |
case is_mim_or_ct_running() of |
158 |
|
true -> |
159 |
1 |
erlang:raise(Class, Reason, StackTrace); |
160 |
|
false -> |
161 |
:-( |
?LOG_CRITICAL(#{what => mim_initialization_aborted, |
162 |
|
text => <<"mongooseim initialization was aborted " |
163 |
|
"because a module start failed.">>, |
164 |
|
class => Class, reason => Reason, |
165 |
:-( |
stacktrace => StackTrace}), |
166 |
:-( |
timer:sleep(3000), |
167 |
:-( |
erlang:halt(string:substr(lists:flatten(ErrorText), |
168 |
|
1, 199)) |
169 |
|
end |
170 |
|
end. |
171 |
|
|
172 |
|
check_dynamic_domains_support(HostType, Module) -> |
173 |
4476 |
case lists:member(HostType, ?MYHOSTS) of |
174 |
3563 |
true -> ok; |
175 |
|
false -> |
176 |
913 |
case gen_mod:does_module_support(Module, dynamic_domains) of |
177 |
913 |
true -> ok; |
178 |
|
false -> |
179 |
:-( |
error({Module, HostType, dynamic_domains_feature_is_not_supported}) |
180 |
|
end |
181 |
|
end. |
182 |
|
|
183 |
|
is_mim_or_ct_running() -> |
184 |
1 |
?MODULE:is_app_running(mongooseim) |
185 |
|
%% Common tests would be very confused if we kill the whole node |
186 |
:-( |
orelse is_common_test_running(). |
187 |
|
|
188 |
|
is_common_test_running() -> |
189 |
:-( |
try |
190 |
:-( |
is_list(ct:get_status()) |
191 |
|
catch _:_ -> |
192 |
:-( |
false |
193 |
|
end. |
194 |
|
|
195 |
|
-spec start_backend_module(module(), list()) -> any(). |
196 |
|
start_backend_module(Module, Opts) -> |
197 |
21 |
start_backend_module(Module, Opts, []). |
198 |
|
|
199 |
|
start_backend_module(Module, Opts, TrackedFuncs) -> |
200 |
365 |
Backend = gen_mod:get_opt(backend, Opts, mnesia), |
201 |
365 |
backend_module:create(Module, Backend, TrackedFuncs). |
202 |
|
|
203 |
|
-spec is_app_running(_) -> boolean(). |
204 |
|
is_app_running(AppName) -> |
205 |
|
%% Use a high timeout to prevent a false positive in a high load system |
206 |
1 |
Timeout = 15000, |
207 |
1 |
lists:keymember(AppName, 1, application:which_applications(Timeout)). |
208 |
|
|
209 |
|
%% @doc This function should be called by mongoose_modules only. |
210 |
|
%% To stop a module at runtime, use mongoose_modules:ensure_stopped/2 instead. |
211 |
|
-spec stop_module(host_type(), module()) -> ok. |
212 |
|
stop_module(HostType, Module) -> |
213 |
4456 |
assert_loaded(HostType, Module), |
214 |
4456 |
stop_module_for_host_type(HostType, Module). |
215 |
|
|
216 |
|
-spec stop_module_for_host_type(host_type(), module()) -> ok. |
217 |
|
stop_module_for_host_type(HostType, Module) -> |
218 |
4456 |
try Module:stop(HostType) of |
219 |
|
{wait, ProcList} when is_list(ProcList) -> |
220 |
:-( |
lists:foreach(fun wait_for_process/1, ProcList); |
221 |
|
{wait, Process} -> |
222 |
:-( |
wait_for_process(Process); |
223 |
|
_ -> |
224 |
4456 |
ok |
225 |
|
catch Class:Reason:Stacktrace -> |
226 |
:-( |
?LOG_ERROR(#{what => module_stopping_failed, |
227 |
|
host_type => HostType, stop_module => Module, |
228 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
229 |
:-( |
erlang:raise(Class, Reason, Stacktrace) |
230 |
|
end. |
231 |
|
|
232 |
|
-spec does_module_support(module(), module_feature()) -> boolean(). |
233 |
|
does_module_support(Module, Feature) -> |
234 |
1825 |
lists:member(Feature, get_supported_features(Module)). |
235 |
|
|
236 |
|
-spec get_supported_features(module()) -> [module_feature()]. |
237 |
|
get_supported_features(Module) -> |
238 |
|
%% if module is not loaded, erlang:function_exported/3 returns false |
239 |
1825 |
case erlang:function_exported(Module, supported_features, 0) of |
240 |
1825 |
true -> apply(Module, supported_features, []); |
241 |
:-( |
false -> [] |
242 |
|
end. |
243 |
|
|
244 |
|
-spec config_spec(module()) -> mongoose_config_spec:config_section(). |
245 |
|
config_spec(Module) -> |
246 |
5600 |
Module:config_spec(). |
247 |
|
|
248 |
|
-spec wait_for_process(atom() | pid() | {atom(), atom()}) -> 'ok'. |
249 |
|
wait_for_process(Process) -> |
250 |
:-( |
MonitorReference = erlang:monitor(process, Process), |
251 |
:-( |
case wait_for_stop(MonitorReference) of |
252 |
:-( |
ok -> ok; |
253 |
|
timeout -> |
254 |
:-( |
catch exit(whereis(Process), kill), |
255 |
:-( |
wait_for_stop(MonitorReference), |
256 |
:-( |
ok |
257 |
|
end. |
258 |
|
|
259 |
|
-spec wait_for_stop(reference()) -> 'ok' | timeout. |
260 |
|
wait_for_stop(MonitorReference) -> |
261 |
:-( |
receive |
262 |
|
{'DOWN', MonitorReference, _Type, _Object, _Info} -> |
263 |
:-( |
ok |
264 |
|
after 5000 -> |
265 |
:-( |
timeout |
266 |
|
end. |
267 |
|
|
268 |
|
get_opt(Opt, Opts) -> |
269 |
1137 |
case lists:keysearch(Opt, 1, Opts) of |
270 |
|
false -> |
271 |
:-( |
throw({undefined_option, Opt}); |
272 |
|
{value, {_, Val}} -> |
273 |
1137 |
Val |
274 |
|
end. |
275 |
|
|
276 |
|
get_opt(Opt, Opts, Default) -> |
277 |
31604 |
case lists:keysearch(Opt, 1, Opts) of |
278 |
|
false -> |
279 |
22250 |
Default; |
280 |
|
{value, {_, Val}} -> |
281 |
9354 |
Val |
282 |
|
end. |
283 |
|
|
284 |
|
get_opt(Opt, Opts, F, Default) -> |
285 |
322 |
case lists:keysearch(Opt, 1, Opts) of |
286 |
|
false -> |
287 |
253 |
Default; |
288 |
|
{value, {_, Val}} -> |
289 |
69 |
F(Val) |
290 |
|
end. |
291 |
|
|
292 |
|
-spec set_opt(_, [tuple()], _) -> [tuple(), ...]. |
293 |
|
set_opt(Opt, Opts, Value) -> |
294 |
568 |
lists:keystore(Opt, 1, Opts, {Opt, Value}). |
295 |
|
|
296 |
|
|
297 |
|
%%% TODO Make Opt an atom. Fix in mod_auth_token: |
298 |
|
%%% 374: The call gen_mod:get_module_opt(Domain::any(), 'mod_auth_token', |
299 |
|
%%% {'validity_period','access' | 'refresh'}, {1 | 25,'days' | 'hours'}) |
300 |
|
%%% breaks the contract (mongooseim:host_type(), module(), atom(), term()) -> term() |
301 |
|
-spec get_module_opt(mongooseim:host_type(), module(), term(), term()) -> term(). |
302 |
|
get_module_opt(HostType, Module, Opt, Default) -> |
303 |
|
%% Fail in dev builds. |
304 |
|
%% It protects against passing something weird as a Module argument |
305 |
|
%% or against wrong argument order. |
306 |
14589 |
?ASSERT_MODULE(Module), |
307 |
14589 |
ModuleOpts = get_module_opts(HostType, Module), |
308 |
14589 |
get_opt(Opt, ModuleOpts, Default). |
309 |
|
|
310 |
|
|
311 |
|
get_module_opts(HostType, Module) -> |
312 |
15345 |
mongoose_config:get_opt([{modules, HostType}, Module], []). |
313 |
|
|
314 |
|
get_loaded_module_opts(HostType, Module) -> |
315 |
88 |
mongoose_config:get_opt([{modules, HostType}, Module]). |
316 |
|
|
317 |
|
-spec get_opt_subhost(domain_name(), |
318 |
|
list(), |
319 |
|
mongoose_subdomain_utils:subdomain_pattern()) -> |
320 |
|
domain_name(). |
321 |
|
get_opt_subhost(Host, Opts, Default) -> |
322 |
|
%% TODO: try to get rid of this interface |
323 |
120 |
Val = get_opt(host, Opts, Default), |
324 |
120 |
mongoose_subdomain_utils:get_fqdn(Val, Host). |
325 |
|
|
326 |
|
-spec get_module_opt_subhost(domain_name(), |
327 |
|
module(), |
328 |
|
mongoose_subdomain_utils:subdomain_pattern()) -> |
329 |
|
domain_name(). |
330 |
|
get_module_opt_subhost(Host, Module, Default) -> |
331 |
|
%% TODO: try to get rid of this interface |
332 |
|
%% note that get_module_opt/4 requires host_type(), while |
333 |
|
%% mongoose_subdomain_utils:get_fqdn/2 expects domain_name() |
334 |
22 |
Spec = get_module_opt(Host, Module, host, Default), |
335 |
22 |
mongoose_subdomain_utils:get_fqdn(Spec, Host). |
336 |
|
|
337 |
|
-spec loaded_modules() -> [module()]. |
338 |
|
loaded_modules() -> |
339 |
3 |
lists:usort(lists:flatmap(fun loaded_modules/1, ?ALL_HOST_TYPES)). |
340 |
|
|
341 |
|
-spec loaded_modules(host_type()) -> [module()]. |
342 |
|
loaded_modules(HostType) -> |
343 |
511 |
maps:keys(mongoose_config:get_opt({modules, HostType})). |
344 |
|
|
345 |
|
-spec loaded_modules_with_opts(host_type()) -> #{module() => module_opts()}. |
346 |
|
loaded_modules_with_opts(HostType) -> |
347 |
20825 |
mongoose_config:get_opt({modules, HostType}). |
348 |
|
|
349 |
|
-spec loaded_modules_with_opts() -> #{host_type() => #{module() => module_opts()}}. |
350 |
|
loaded_modules_with_opts() -> |
351 |
:-( |
maps:from_list([{HostType, loaded_modules_with_opts(HostType)} || HostType <- ?ALL_HOST_TYPES]). |
352 |
|
|
353 |
|
-spec hosts_with_module(module()) -> [host_type()]. |
354 |
|
hosts_with_module(Module) -> |
355 |
20 |
[HostType || HostType <- ?ALL_HOST_TYPES, is_loaded(HostType, Module)]. |
356 |
|
|
357 |
|
-spec hosts_and_opts_with_module(module()) -> #{host_type() => module_opts()}. |
358 |
|
hosts_and_opts_with_module(Module) -> |
359 |
:-( |
maps:from_list( |
360 |
|
lists:flatmap(fun(HostType) -> |
361 |
:-( |
case mongoose_config:lookup_opt([{modules, HostType}, Module]) of |
362 |
:-( |
{error, not_found} -> []; |
363 |
:-( |
{ok, Opts} -> [{HostType, Opts}] |
364 |
|
end |
365 |
|
end, ?ALL_HOST_TYPES)). |
366 |
|
|
367 |
|
-spec get_module_proc(binary() | string(), module()) -> atom(). |
368 |
|
%% TODO: |
369 |
|
%% split this interface into 2: |
370 |
|
%% * create_module_proc_name/2 - which can create new atoms by calling list_to_atom/1 |
371 |
|
%% * get_module_proc_name/2 - which should use safe list_to_existing_atom/1 function |
372 |
|
get_module_proc(Host, Base) when is_binary(Host) -> |
373 |
27637 |
get_module_proc(binary_to_list(Host), Base); |
374 |
|
get_module_proc(Host, Base) -> |
375 |
28617 |
list_to_atom(atom_to_list(Base) ++ "_" ++ Host). |
376 |
|
|
377 |
|
-spec assert_loaded(mongooseim:host_type(), module()) -> ok. |
378 |
|
assert_loaded(HostType, Module) -> |
379 |
8932 |
case is_loaded(HostType, Module) of |
380 |
|
true -> |
381 |
8932 |
ok; |
382 |
|
false -> |
383 |
:-( |
error(#{what => module_not_loaded, |
384 |
|
text => <<"Module missing from mongoose_config">>, |
385 |
|
host_type => HostType, |
386 |
|
module => Module}) |
387 |
|
end. |
388 |
|
|
389 |
|
-spec is_loaded(HostType :: binary(), Module :: atom()) -> boolean(). |
390 |
|
is_loaded(HostType, Module) -> |
391 |
20554 |
maps:is_key(Module, loaded_modules_with_opts(HostType)). |
392 |
|
|
393 |
|
-spec get_deps(HostType :: host_type(), Module :: module(), |
394 |
|
Opts :: proplists:proplist()) -> module_deps_list(). |
395 |
|
get_deps(HostType, Module, Opts) -> |
396 |
|
%% the module has to be loaded, |
397 |
|
%% otherwise the erlang:function_exported/3 returns false |
398 |
27507 |
code:ensure_loaded(Module), |
399 |
27507 |
case erlang:function_exported(Module, deps, 2) of |
400 |
|
true -> |
401 |
918 |
Deps = Module:deps(HostType, Opts), |
402 |
918 |
lists:filter(fun(D) -> element(1, D) =/= service end, Deps); |
403 |
|
_ -> |
404 |
26589 |
[] |
405 |
|
end. |
406 |
|
|
407 |
|
-spec get_required_services(host_type(), module(), proplists:proplist()) -> |
408 |
|
service_deps_list(). |
409 |
|
get_required_services(HostType, Module, Options) -> |
410 |
|
%% the module has to be loaded, |
411 |
|
%% otherwise the erlang:function_exported/3 returns false |
412 |
4476 |
code:ensure_loaded(Module), |
413 |
4476 |
case erlang:function_exported(Module, deps, 2) of |
414 |
|
true -> |
415 |
165 |
[Service || {service, Service} <- Module:deps(HostType, Options)]; |
416 |
|
_ -> |
417 |
4311 |
[] |
418 |
|
end. |