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 |
|
-export_type([key_path/0, opt_key/0, opt_value/0, module_opts/0]). |
30 |
|
|
31 |
|
-export([ |
32 |
|
% Modules start & stop, do NOT use in the tests, use mongoose_modules API instead |
33 |
|
start_module/3, |
34 |
|
stop_module/2, |
35 |
|
does_module_support/2, |
36 |
|
config_spec/1, |
37 |
|
% Get/set opts by host or from a list |
38 |
|
get_opt/2, |
39 |
|
get_opt/3, |
40 |
|
lookup_module_opt/3, |
41 |
|
get_module_opt/3, |
42 |
|
get_module_opt/4, |
43 |
|
get_module_opts/2, |
44 |
|
get_loaded_module_opts/2, |
45 |
|
|
46 |
|
loaded_modules/0, |
47 |
|
loaded_modules/1, |
48 |
|
loaded_modules_with_opts/0, |
49 |
|
loaded_modules_with_opts/1, |
50 |
|
hosts_with_module/1, |
51 |
|
hosts_and_opts_with_module/1, |
52 |
|
get_module_proc/2, |
53 |
|
is_loaded/2, |
54 |
|
get_deps/3]). |
55 |
|
|
56 |
|
-export([is_app_running/1]). % we have to mock it in some tests |
57 |
|
|
58 |
|
-ignore_xref([loaded_modules_with_opts/0, |
59 |
|
loaded_modules_with_opts/1, hosts_and_opts_with_module/1]). |
60 |
|
|
61 |
|
-include("mongoose.hrl"). |
62 |
|
|
63 |
|
-type module_feature() :: atom(). |
64 |
|
-type host_type() :: mongooseim:host_type(). |
65 |
|
-type key_path() :: mongoose_config:key_path(). |
66 |
|
-type opt_key() :: atom(). |
67 |
|
-type opt_value() :: mongoose_config:value(). |
68 |
|
-type module_opts() :: #{opt_key() => opt_value()}. |
69 |
|
|
70 |
|
-callback start(host_type(), module_opts()) -> any(). |
71 |
|
-callback stop(host_type()) -> any(). |
72 |
|
-callback hooks(host_type()) -> gen_hook:hook_list(). |
73 |
|
-callback supported_features() -> [module_feature()]. |
74 |
|
-callback config_spec() -> mongoose_config_spec:config_section(). |
75 |
|
-callback instrumentation(host_type()) -> [mongoose_instrument:spec()]. |
76 |
|
|
77 |
|
%% Optional callback specifying module dependencies. |
78 |
|
%% The dependent module can specify parameters with which the dependee should be |
79 |
|
%% started (the parameters will be merged with params given in user config and |
80 |
|
%% by other modules). |
81 |
|
%% The last element of the tuple specifies whether the ordering can be broken in |
82 |
|
%% case of cycle (in that case soft dependency may be started after the |
83 |
|
%% dependent module). |
84 |
|
%% |
85 |
|
%% TODO: think about getting rid of HostType param for deps/2 interface, currently |
86 |
|
%% it's used only by global_distrib modules (see mod_global_distrib_utils:deps/4 |
87 |
|
%% function). |
88 |
|
-callback deps(host_type(), module_opts()) -> gen_mod_deps:deps(). |
89 |
|
|
90 |
|
-optional_callbacks([hooks/1, config_spec/0, supported_features/0, instrumentation/1, deps/2]). |
91 |
|
|
92 |
|
%% @doc This function should be called by mongoose_modules only. |
93 |
|
%% To start a new module at runtime, use mongoose_modules:ensure_module/3 instead. |
94 |
|
-spec start_module(host_type(), module(), module_opts()) -> {ok, term()}. |
95 |
|
start_module(HostType, Module, Opts) -> |
96 |
5338 |
assert_loaded(HostType, Module), |
97 |
5338 |
start_module_for_host_type(HostType, Module, Opts). |
98 |
|
|
99 |
|
start_module_for_host_type(HostType, Module, Opts) -> |
100 |
5338 |
{links, LinksBefore} = erlang:process_info(self(), links), |
101 |
5338 |
try |
102 |
5338 |
lists:map(fun mongoose_service:assert_loaded/1, |
103 |
|
get_required_services(HostType, Module, Opts)), |
104 |
5338 |
check_dynamic_domains_support(HostType, Module), |
105 |
5338 |
run_for_instrumentation(HostType, fun mongoose_instrument:set_up/1, Module), |
106 |
5338 |
Res = Module:start(HostType, Opts), |
107 |
5338 |
run_for_hooks(HostType, fun gen_hook:add_handlers/1, Module), |
108 |
5338 |
{links, LinksAfter} = erlang:process_info(self(), links), |
109 |
5338 |
case lists:sort(LinksBefore) =:= lists:sort(LinksAfter) of |
110 |
5338 |
true -> ok; |
111 |
|
false -> |
112 |
|
%% TODO: grepping for "fail_ci_build=true" is bad option |
113 |
|
%% for ci testing, rework this. |
114 |
:-( |
CIInfo = "fail_ci_build=true ", |
115 |
|
%% Note for programmers: |
116 |
|
%% Never call start_link directly from your_module:start/2 function! |
117 |
|
%% The process will be killed if we start modules remotely or in shell |
118 |
:-( |
?LOG_ERROR(#{what => unexpected_links, ci_info => CIInfo, |
119 |
:-( |
links_before => LinksBefore, links_after => LinksAfter}) |
120 |
|
end, |
121 |
5338 |
?LOG_DEBUG(#{what => module_started, module => Module, host_type => HostType}), |
122 |
|
% normalise result |
123 |
5338 |
case Res of |
124 |
459 |
{ok, R} -> {ok, R}; |
125 |
4879 |
_ -> {ok, Res} |
126 |
|
end |
127 |
|
catch |
128 |
|
Class:Reason:StackTrace -> |
129 |
:-( |
ErrorText = io_lib:format("Problem starting the module ~p for " |
130 |
|
"host_type ~p~n options: ~p~n ~p: ~p~n~p", |
131 |
|
[Module, HostType, Opts, Class, Reason, |
132 |
|
StackTrace]), |
133 |
:-( |
?LOG_CRITICAL(#{what => module_start_failed, module => Module, |
134 |
|
host_type => HostType, opts => Opts, class => Class, |
135 |
:-( |
reason => Reason, stacktrace => StackTrace}), |
136 |
:-( |
case is_mim_or_ct_running() of |
137 |
|
true -> |
138 |
:-( |
erlang:raise(Class, Reason, StackTrace); |
139 |
|
false -> |
140 |
:-( |
?LOG_CRITICAL(#{what => mim_initialization_aborted, |
141 |
|
text => <<"mongooseim initialization was aborted " |
142 |
|
"because a module start failed.">>, |
143 |
|
class => Class, reason => Reason, |
144 |
:-( |
stacktrace => StackTrace}), |
145 |
:-( |
timer:sleep(3000), |
146 |
:-( |
erlang:halt(string:substr(lists:flatten(ErrorText), |
147 |
|
1, 199)) |
148 |
|
end |
149 |
|
end. |
150 |
|
|
151 |
|
run_for_hooks(HostType, Fun, Module) -> |
152 |
10664 |
case erlang:function_exported(Module, hooks, 1) of |
153 |
8118 |
true -> Fun(Module:hooks(HostType)); |
154 |
2546 |
false -> ok |
155 |
|
end. |
156 |
|
|
157 |
|
run_for_instrumentation(HostType, Fun, Module) -> |
158 |
10664 |
case erlang:function_exported(Module, instrumentation, 1) of |
159 |
512 |
true -> Fun(Module:instrumentation(HostType)); |
160 |
10152 |
false -> ok |
161 |
|
end. |
162 |
|
|
163 |
|
check_dynamic_domains_support(HostType, Module) -> |
164 |
5338 |
case lists:member(HostType, ?MYHOSTS) of |
165 |
3048 |
true -> ok; |
166 |
|
false -> |
167 |
2290 |
case gen_mod:does_module_support(Module, dynamic_domains) of |
168 |
2290 |
true -> ok; |
169 |
|
false -> |
170 |
:-( |
error({Module, HostType, dynamic_domains_feature_is_not_supported}) |
171 |
|
end |
172 |
|
end. |
173 |
|
|
174 |
|
is_mim_or_ct_running() -> |
175 |
:-( |
?MODULE:is_app_running(mongooseim) |
176 |
|
%% Common tests would be very confused if we kill the whole node |
177 |
:-( |
orelse is_common_test_running(). |
178 |
|
|
179 |
|
is_common_test_running() -> |
180 |
:-( |
try |
181 |
:-( |
is_list(ct:get_status()) |
182 |
|
catch _:_ -> |
183 |
:-( |
false |
184 |
|
end. |
185 |
|
|
186 |
|
-spec is_app_running(_) -> boolean(). |
187 |
|
is_app_running(AppName) -> |
188 |
|
%% Use a high timeout to prevent a false positive in a high load system |
189 |
:-( |
Timeout = 15000, |
190 |
:-( |
lists:keymember(AppName, 1, application:which_applications(Timeout)). |
191 |
|
|
192 |
|
%% @doc This function should be called by mongoose_modules only. |
193 |
|
%% To stop a module at runtime, use mongoose_modules:ensure_stopped/2 instead. |
194 |
|
-spec stop_module(host_type(), module()) -> ok. |
195 |
|
stop_module(HostType, Module) -> |
196 |
5326 |
assert_loaded(HostType, Module), |
197 |
5326 |
stop_module_for_host_type(HostType, Module). |
198 |
|
|
199 |
|
-spec stop_module_for_host_type(host_type(), module()) -> ok. |
200 |
|
stop_module_for_host_type(HostType, Module) -> |
201 |
5326 |
run_for_hooks(HostType, fun gen_hook:delete_handlers/1, Module), |
202 |
5326 |
try Module:stop(HostType) of |
203 |
|
{wait, ProcList} when is_list(ProcList) -> |
204 |
:-( |
lists:foreach(fun wait_for_process/1, ProcList); |
205 |
|
{wait, Process} -> |
206 |
:-( |
wait_for_process(Process); |
207 |
|
_ -> |
208 |
5326 |
ok |
209 |
|
catch Class:Reason:Stacktrace -> |
210 |
:-( |
?LOG_ERROR(#{what => module_stopping_failed, |
211 |
|
host_type => HostType, stop_module => Module, |
212 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
213 |
:-( |
erlang:raise(Class, Reason, Stacktrace) |
214 |
|
end, |
215 |
5326 |
run_for_instrumentation(HostType, fun mongoose_instrument:tear_down/1, Module). |
216 |
|
|
217 |
|
-spec does_module_support(module(), module_feature()) -> boolean(). |
218 |
|
does_module_support(Module, Feature) -> |
219 |
3340 |
lists:member(Feature, get_supported_features(Module)). |
220 |
|
|
221 |
|
-spec get_supported_features(module()) -> [module_feature()]. |
222 |
|
get_supported_features(Module) -> |
223 |
|
%% if module is not loaded, erlang:function_exported/3 returns false |
224 |
3340 |
case erlang:function_exported(Module, supported_features, 0) of |
225 |
3333 |
true -> apply(Module, supported_features, []); |
226 |
7 |
false -> [] |
227 |
|
end. |
228 |
|
|
229 |
|
-spec config_spec(module()) -> mongoose_config_spec:config_section(). |
230 |
|
config_spec(Module) -> |
231 |
7068 |
Module:config_spec(). |
232 |
|
|
233 |
|
-spec wait_for_process(atom() | pid() | {atom(), atom()}) -> 'ok'. |
234 |
|
wait_for_process(Process) -> |
235 |
:-( |
MonitorReference = erlang:monitor(process, Process), |
236 |
:-( |
case wait_for_stop(MonitorReference) of |
237 |
:-( |
ok -> ok; |
238 |
|
timeout -> |
239 |
:-( |
catch exit(whereis(Process), kill), |
240 |
:-( |
wait_for_stop(MonitorReference), |
241 |
:-( |
ok |
242 |
|
end. |
243 |
|
|
244 |
|
-spec wait_for_stop(reference()) -> 'ok' | timeout. |
245 |
|
wait_for_stop(MonitorReference) -> |
246 |
:-( |
receive |
247 |
|
{'DOWN', MonitorReference, _Type, _Object, _Info} -> |
248 |
:-( |
ok |
249 |
|
after 5000 -> |
250 |
:-( |
timeout |
251 |
|
end. |
252 |
|
|
253 |
|
-spec get_opt(opt_key() | key_path(), module_opts()) -> opt_value(). |
254 |
|
get_opt(Path, Opts) when is_list(Path), is_map(Opts) -> |
255 |
:-( |
lists:foldl(fun maps:get/2, Opts, Path); |
256 |
|
get_opt(Opt, Opts) when is_map(Opts) -> |
257 |
6331 |
maps:get(Opt, Opts). |
258 |
|
|
259 |
|
-spec get_opt(opt_key() | key_path(), module_opts(), opt_value()) -> opt_value(). |
260 |
|
get_opt(Path, Opts, Default) -> |
261 |
3957 |
try |
262 |
3957 |
get_opt(Path, Opts) |
263 |
|
catch |
264 |
469 |
error:{badkey, _} -> Default |
265 |
|
end. |
266 |
|
|
267 |
|
-spec lookup_module_opt(mongooseim:host_type(), module(), opt_key() | key_path()) -> |
268 |
|
{ok, opt_value()} | {error, not_found}. |
269 |
|
lookup_module_opt(HostType, Module, Key) -> |
270 |
13 |
mongoose_config:lookup_opt(config_path(HostType, Module, Key)). |
271 |
|
|
272 |
|
-spec get_module_opt(mongooseim:host_type(), module(), opt_key() | key_path(), opt_value()) -> |
273 |
|
opt_value(). |
274 |
|
get_module_opt(HostType, Module, Key, Default) -> |
275 |
|
%% Fail in dev builds. |
276 |
|
%% It protects against passing something weird as a Module argument |
277 |
|
%% or against wrong argument order. |
278 |
43307 |
?ASSERT_MODULE(Module), |
279 |
43307 |
mongoose_config:get_opt(config_path(HostType, Module, Key), Default). |
280 |
|
|
281 |
|
-spec get_module_opt(mongooseim:host_type(), module(), opt_key() | key_path()) -> opt_value(). |
282 |
|
get_module_opt(HostType, Module, Key) -> |
283 |
123928 |
mongoose_config:get_opt(config_path(HostType, Module, Key)). |
284 |
|
|
285 |
|
-spec config_path(mongooseim:host_type(), module(), opt_key() | key_path()) -> key_path(). |
286 |
|
config_path(HostType, Module, Path) when is_list(Path) -> |
287 |
122 |
[{modules, HostType}, Module] ++ Path; |
288 |
|
config_path(HostType, Module, Key) when is_atom(Key) -> |
289 |
167126 |
[{modules, HostType}, Module, Key]. |
290 |
|
|
291 |
|
-spec get_module_opts(mongooseim:host_type(), module()) -> module_opts(). |
292 |
|
get_module_opts(HostType, Module) -> |
293 |
985 |
?ASSERT_MODULE(Module), |
294 |
985 |
mongoose_config:get_opt([{modules, HostType}, Module], #{}). |
295 |
|
|
296 |
|
-spec get_loaded_module_opts(mongooseim:host_type(), module()) -> module_opts(). |
297 |
|
get_loaded_module_opts(HostType, Module) -> |
298 |
141 |
mongoose_config:get_opt([{modules, HostType}, Module]). |
299 |
|
|
300 |
|
-spec loaded_modules() -> [module()]. |
301 |
|
loaded_modules() -> |
302 |
10 |
lists:usort(lists:flatmap(fun loaded_modules/1, ?ALL_HOST_TYPES)). |
303 |
|
|
304 |
|
-spec loaded_modules(host_type()) -> [module()]. |
305 |
|
loaded_modules(HostType) -> |
306 |
513 |
maps:keys(mongoose_config:get_opt({modules, HostType})). |
307 |
|
|
308 |
|
-spec loaded_modules_with_opts(host_type()) -> #{module() => module_opts()}. |
309 |
|
loaded_modules_with_opts(HostType) -> |
310 |
24430 |
mongoose_config:get_opt({modules, HostType}). |
311 |
|
|
312 |
|
-spec loaded_modules_with_opts() -> #{host_type() => #{module() => module_opts()}}. |
313 |
|
loaded_modules_with_opts() -> |
314 |
:-( |
maps:from_list([{HostType, loaded_modules_with_opts(HostType)} || HostType <- ?ALL_HOST_TYPES]). |
315 |
|
|
316 |
|
-spec hosts_with_module(module()) -> [host_type()]. |
317 |
|
hosts_with_module(Module) -> |
318 |
28 |
[HostType || HostType <- ?ALL_HOST_TYPES, is_loaded(HostType, Module)]. |
319 |
|
|
320 |
|
-spec hosts_and_opts_with_module(module()) -> #{host_type() => module_opts()}. |
321 |
|
hosts_and_opts_with_module(Module) -> |
322 |
:-( |
maps:from_list( |
323 |
|
lists:flatmap(fun(HostType) -> |
324 |
:-( |
case mongoose_config:lookup_opt([{modules, HostType}, Module]) of |
325 |
:-( |
{error, not_found} -> []; |
326 |
:-( |
{ok, Opts} -> [{HostType, Opts}] |
327 |
|
end |
328 |
|
end, ?ALL_HOST_TYPES)). |
329 |
|
|
330 |
|
-spec get_module_proc(binary() | string(), module()) -> atom(). |
331 |
|
%% TODO: |
332 |
|
%% split this interface into 2: |
333 |
|
%% * create_module_proc_name/2 - which can create new atoms by calling list_to_atom/1 |
334 |
|
%% * get_module_proc_name/2 - which should use safe list_to_existing_atom/1 function |
335 |
|
get_module_proc(Host, Base) when is_binary(Host) -> |
336 |
36629 |
get_module_proc(binary_to_list(Host), Base); |
337 |
|
get_module_proc(Host, Base) -> |
338 |
36629 |
list_to_atom(atom_to_list(Base) ++ "_" ++ Host). |
339 |
|
|
340 |
|
-spec assert_loaded(mongooseim:host_type(), module()) -> ok. |
341 |
|
assert_loaded(HostType, Module) -> |
342 |
10664 |
case is_loaded(HostType, Module) of |
343 |
|
true -> |
344 |
10664 |
ok; |
345 |
|
false -> |
346 |
:-( |
error(#{what => module_not_loaded, |
347 |
|
text => <<"Module missing from mongoose_config">>, |
348 |
|
host_type => HostType, |
349 |
|
module => Module}) |
350 |
|
end. |
351 |
|
|
352 |
|
-spec is_loaded(HostType :: binary(), Module :: atom()) -> boolean(). |
353 |
|
is_loaded(HostType, Module) -> |
354 |
23882 |
maps:is_key(Module, loaded_modules_with_opts(HostType)). |
355 |
|
|
356 |
|
-spec get_deps(host_type(), module(), module_opts()) -> gen_mod_deps:module_deps(). |
357 |
|
get_deps(HostType, Module, Opts) -> |
358 |
111504 |
case mongoose_lib:is_exported(Module, deps, 2) of |
359 |
|
true -> |
360 |
7280 |
Deps = Module:deps(HostType, Opts), |
361 |
7280 |
lists:filter(fun(D) -> element(1, D) =/= service end, Deps); |
362 |
|
_ -> |
363 |
104224 |
[] |
364 |
|
end. |
365 |
|
|
366 |
|
-spec get_required_services(host_type(), module(), module_opts()) -> [mongoose_service:service()]. |
367 |
|
get_required_services(HostType, Module, Options) -> |
368 |
5338 |
case mongoose_lib:is_exported(Module, deps, 2) of |
369 |
|
true -> |
370 |
286 |
[Service || {service, Service} <- Module:deps(HostType, Options)]; |
371 |
|
_ -> |
372 |
5052 |
[] |
373 |
|
end. |