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