/home/runner/work/eturnal/eturnal/_build/test/cover/ct/eturnal_module.html

1 %%% eturnal STUN/TURN server.
2 %%%
3 %%% Copyright (c) 2020-2023 Holger Weiss <holger@zedat.fu-berlin.de>.
4 %%% Copyright (c) 2020-2023 ProcessOne, SARL.
5 %%% All rights reserved.
6 %%%
7 %%% Licensed under the Apache License, Version 2.0 (the "License");
8 %%% you may not use this file except in compliance with the License.
9 %%% You may obtain a copy of the License at
10 %%%
11 %%% http://www.apache.org/licenses/LICENSE-2.0
12 %%%
13 %%% Unless required by applicable law or agreed to in writing, software
14 %%% distributed under the License is distributed on an "AS IS" BASIS,
15 %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 %%% See the License for the specific language governing permissions and
17 %%% limitations under the License.
18
19 %%% @doc An eturnal module adds functionality to the eturnal server. It is to be
20 %%% named `mod_foo', where `foo' describes the added functionality. The module
21 %%% may export `start/0', `stop/0', `handle_event/2', and `options/0' functions.
22 %%%
23 %%% If a `start/0' callback is exported, it must return `ok' or `{ok, Events}',
24 %%% where `Events' is either a single {@link event()} or a list of {@link
25 %%% events()} the module is interested in. Currently, the following events may
26 %%% be triggered: `stun_query', `turn_session_start', `turn_session_stop', and
27 %%% `protocol_error'.
28 %%%
29 %%% If a `start/0' function is exported and subscribes to one or more events, a
30 %%% `handle_event/2' callback <em>must</em> be exported as well. It is called
31 %%% with the {@link event()} name as the first argument and an {@link info()}
32 %%% map with related data as the second. The contents of that map depend on the
33 %%% event. Note that the `handle_event/2' function is executed in the context of
34 %%% the process handling the STUN/TURN session, so it should never block. If it
35 %%% might, and/or if it needs some state, one or more handler processes must be
36 %%% created.
37 %%%
38 %%% The `options/0' callback returns an {@link options()} tuple with two
39 %%% elements. The first is a map of module configuration options, where the keys
40 %%% are the {@link option()} names and the values are functions that validate
41 %%% the option values. Those functions are returned by the <a
42 %%% href="https://hex.pm/packages/yval">yval</a> library, see the documentation
43 %%% for the list of <a href="https://hexdocs.pm/yval/yval.html#index">available
44 %%% validators</a>. The second element is a list of optional tuples to specify
45 %%% any `{required, [Options]}' and/or `{defaults, #{Option => Value}}'. For
46 %%% example:
47 %%%
48 %%% ```
49 %%% options() ->
50 %%% {#{threshold => yval:pos_int()},
51 %%% [{defaults,
52 %%% #{threshold => 42}}]}.
53 %%% '''
54 %%%
55 %%% The option values are queried by calling {@link eturnal_module:get_opt/2}
56 %%% with the module name as the first and the {@link option()} name as the
57 %%% second argument. Note that the lookup is very efficient, so there's no point
58 %%% in saving option values into some state. If the module has no configuration
59 %%% options, the `options/0' function may be omitted.
60 %%%
61 %%% The optional `stop/0' callback must return `ok'. Note that the `start/0' and
62 %%% `stop/0' functions might not just be called on eturnal startup and shutdown,
63 %%% but also on configuration reloads.
64 %%%
65 %%% If the module depends on other applications, those must be added to the
66 %%% `rebar.config' file, but not to the app file. They are to be started by
67 %%% calling {@link eturnal_module:ensure_deps/2}, where the first argument is
68 %%% the module name and the second is a list of dependency names. Note that
69 %%% there's no need to list transitive dependencies.
70 %%%
71 %%% The module is enabled by adding its configuration to the `modules' section
72 %%% of eturnal's configuration file as described in `doc/overview.edoc'. The
73 %%% module configuration options are to be documented in that file as well.
74
75 -module(eturnal_module).
76 -export([init/0,
77 terminate/0,
78 start/1,
79 stop/1,
80 handle_event/2,
81 options/1,
82 get_opt/2,
83 ensure_deps/2]).
84 -export_type([dep/0,
85 event/0,
86 events/0,
87 info/0,
88 option/0,
89 options/0]).
90
91 -type dep() :: atom().
92 -type event() :: atom().
93 -type events() :: [event()].
94 -type info() :: #{atom() => term()}.
95 -type option() :: atom().
96 -type options() :: {yval:validators(), [yval:validator_option()]}.
97
98 -callback start() -> ok | {ok, event() | [event()]}.
99 -callback stop() -> ok.
100 -callback handle_event(event(), info()) -> ok.
101 -callback options() -> options().
102
103 -optional_callbacks([start/0, stop/0, handle_event/2, options/0]).
104
105 -ifdef(EUNIT).
106 -include_lib("eunit/include/eunit.hrl").
107 -endif.
108 -include_lib("kernel/include/logger.hrl").
109 -ifdef(old_persistent_term).
110 -define(m(Name), {m, Name}).
111 -define(e(Name), {e, Name}).
112 -else.
113 -define(m(Name), {?MODULE, m, Name}).
114 -define(e(Name), {?MODULE, e, Name}).
115 -endif.
116
117 %% API.
118
119 -spec init() -> ok.
120 -ifdef(old_persistent_term).
121 init() ->
122 events = ets:new(events, [named_table, {read_concurrency, true}]),
123 ok.
124 -else.
125 init() ->
126 1 ok.
127 -endif.
128
129 -spec terminate() -> ok.
130 -ifdef(old_persistent_term).
131 terminate() ->
132 true = ets:delete(events),
133 ok.
134 -else.
135 terminate() ->
136 1 ok.
137 -endif.
138
139 -spec start(module()) -> ok | {error, term()}.
140 start(Mod) ->
141 9 case erlang:function_exported(Mod, start, 0) of
142 true ->
143 9 ?LOG_DEBUG("Calling ~s:start/0", [Mod]),
144 9 try Mod:start() of
145 ok ->
146
:-(
ok;
147 {ok, Events} ->
148 9 ok = subscribe_events(Events, Mod)
149 catch _:Err:Stack ->
150
:-(
?LOG_DEBUG("Module ~s failed at starting:~n~p",
151
:-(
[Mod, Stack]),
152
:-(
{error, Err}
153 end;
154 false ->
155
:-(
?LOG_DEBUG("Module ~s doesn't export start/0", [Mod])
156 end.
157
158 -spec stop(module()) -> ok | {error, term()}.
159 stop(Mod) ->
160 9 ok = unsubscribe_events(Mod),
161 9 case erlang:function_exported(Mod, stop, 0) of
162 true ->
163 9 ?LOG_DEBUG("Calling ~s:stop/1", [Mod]),
164 9 try ok = Mod:stop()
165 catch _:Err:Stack ->
166
:-(
?LOG_DEBUG("Module ~s failed at stopping:~n~p",
167
:-(
[Mod, Stack]),
168
:-(
{error, Err}
169 end;
170 false ->
171
:-(
?LOG_DEBUG("Module ~s doesn't export stop/1", [Mod]),
172
:-(
ok
173 end.
174
175 -spec handle_event(event(), info()) -> ok.
176 handle_event(Event, Info) ->
177 9 ?LOG_DEBUG("Got '~s' event", [Event]),
178 9 ok = lists:foreach(
179 fun(Mod) ->
180 21 ?LOG_DEBUG("Calling ~s:handle_event/2", [Mod]),
181 21 try ok = Mod:handle_event(Event, Info)
182 catch _:_Err:Stack ->
183
:-(
?LOG_ERROR("Module ~s failed at handling '~s':~n~p",
184
:-(
[Mod, Event, Stack])
185 end
186 end, get_subscribers(Event)).
187
188 -spec options(module()) -> options().
189 options(Mod) ->
190 9 case erlang:function_exported(Mod, options, 0) of
191 true ->
192 9 ?LOG_DEBUG("Calling ~s:options/0", [Mod]),
193 9 Mod:options();
194 false ->
195
:-(
?LOG_DEBUG("Module ~s doesn't export options/1", [Mod]),
196
:-(
{#{}, []}
197 end.
198
199 -spec get_opt(module(), option()) -> term().
200 get_opt(Mod, Opt) ->
201 32 #{Mod := #{Opt := Val}} = eturnal:get_opt(modules),
202 32 Val.
203
204 -spec ensure_deps(module(), [dep()]) -> ok.
205 ensure_deps(Mod, Deps) ->
206 6 lists:foreach(fun(Dep) -> ok = ensure_dep(Mod, Dep) end, Deps).
207
208 %% Internal functions.
209
210 -ifdef(old_persistent_term).
211 -spec subscribe_events(event() | [event()], module()) -> ok.
212 subscribe_events(Event, Mod) when is_atom(Event) ->
213 ok = subscribe_events([Event], Mod);
214 subscribe_events(Events, Mod) ->
215 Entries = lists:map(
216 fun(Event) ->
217 case ets:lookup(events, ?e(Event)) of
218 [] ->
219 {?e(Event), [Mod]};
220 [{_, Ms}] ->
221 {?e(Event), ordsets:add_element(Mod, Ms)}
222 end
223 end, Events),
224 true = ets:insert(events, [{?m(Mod), Events} | Entries]),
225 ok.
226
227 -spec unsubscribe_events(module()) -> ok.
228 unsubscribe_events(Mod) ->
229 case ets:lookup(events, ?m(Mod)) of
230 [] ->
231 ok;
232 [{?m(Mod), Es}] ->
233 Entries = lists:map(
234 fun(Event) ->
235 [{_, Ms}] = ets:lookup(events, ?e(Event)),
236 {?e(Event), ordsets:del_element(Mod, Ms)}
237 end, Es),
238 true = ets:insert(events, Entries),
239 true = ets:delete(events, ?m(Mod)),
240 ok
241 end.
242
243 -spec get_subscribers(event()) -> [module()].
244 get_subscribers(Event) ->
245 case ets:lookup(events, ?e(Event)) of
246 [] ->
247 [];
248 [{_, Ms}] ->
249 Ms
250 end.
251 -else.
252 -spec subscribe_events(event() | [event()], module()) -> ok.
253 subscribe_events(Event, Mod) when is_atom(Event) ->
254
:-(
ok = subscribe_events([Event], Mod);
255 subscribe_events(Events, Mod) ->
256 9 ok = persistent_term:put(?m(Mod), Events),
257 9 ok = lists:foreach(
258 fun(Event) ->
259 21 Ms = persistent_term:get(?e(Event), ordsets:new()),
260 21 ok = persistent_term:put(?e(Event),
261 ordsets:add_element(Mod, Ms))
262 end, Events).
263
264 -spec unsubscribe_events(module()) -> ok.
265 unsubscribe_events(Mod) ->
266 9 Es = persistent_term:get(?m(Mod), []),
267 9 _R = persistent_term:erase(?m(Mod)),
268 9 ok = lists:foreach(
269 fun(Event) ->
270 21 Ms = persistent_term:get(?e(Event)),
271 21 ok = persistent_term:put(?e(Event),
272 ordsets:del_element(Mod, Ms))
273 end, Es).
274
275 -spec get_subscribers(event()) -> [module()].
276 get_subscribers(Event) ->
277 9 persistent_term:get(?e(Event), []).
278 -endif.
279
280 -spec ensure_dep(module(), dep()) -> ok.
281 ensure_dep(Mod, Dep) ->
282 6 case application:ensure_all_started(Dep) of
283 {ok, _Apps} ->
284 6 ?LOG_DEBUG("Dependency ~s was available already", [Dep]),
285 6 ok;
286 {error, _Reason1} ->
287
:-(
?LOG_DEBUG("Dependency ~s isn't started, loading it", [Dep]),
288
:-(
case start_app(Dep) of
289 ok ->
290
:-(
?LOG_INFO("Dependency ~s is available", [Dep]),
291
:-(
ok;
292 {error, _Reason2} ->
293
:-(
eturnal:abort({dependency_failure, Mod, Dep})
294 end
295 end.
296
297 -spec start_app(dep()) -> ok | {error, term()}.
298 start_app(App) ->
299
:-(
case load_app(App) of
300 ok ->
301
:-(
?LOG_DEBUG("Loaded ~s, trying to start it", [App]),
302
:-(
case application:ensure_started(App) of
303 ok ->
304
:-(
ok;
305 {error, {not_started, Dep}} ->
306
:-(
?LOG_DEBUG("~s depends on ~s, loading it", [App, Dep]),
307
:-(
case start_app(Dep) of
308 ok ->
309
:-(
start_app(App);
310 {error, _Reason} = Err ->
311
:-(
Err
312 end;
313 {error, Reason} = Err ->
314
:-(
?LOG_DEBUG("Cannot start ~s: ~p", [App, Reason]),
315
:-(
Err
316 end;
317 {error, Reason} = Err ->
318
:-(
?LOG_DEBUG("Cannot load ~s: ~p", [App, Reason]),
319
:-(
Err
320 end.
321
322 -spec load_app(dep()) -> ok | {error, term()}.
323 load_app(App) ->
324
:-(
try
325
:-(
LibDir = code:lib_dir(),
326
:-(
AppDir = lists:max(filelib:wildcard([App, "{,-*}"], LibDir)),
327
:-(
EbinDir = filename:join([LibDir, AppDir, "ebin"]),
328
:-(
AppFile = filename:join(EbinDir, [App, ".app"]),
329
:-(
{ok, [{application, App, Props}]} = file:consult(AppFile),
330
:-(
Mods = proplists:get_value(modules, Props),
331
:-(
true = code:add_path(EbinDir),
332
:-(
case lists:any(fun(Mod) ->
333
:-(
code:module_status(Mod) =:= not_loaded
334 end, Mods) of
335 true ->
336
:-(
?LOG_DEBUG("Loading modules: ~p", [Mods]),
337
:-(
ok = code:atomic_load(Mods);
338 false ->
339
:-(
?LOG_DEBUG("Modules loaded already: ~p", [Mods]),
340
:-(
ok
341 end
342 catch _:Err ->
343
:-(
{error, Err}
344 end.
345
346 %% EUnit tests.
347
348 -ifdef(EUNIT).
349 load_test_() ->
350
:-(
[?_assertEqual(ok, start_app(eunit)),
351
:-(
?_assertMatch({error, _}, start_app(nonexistent))].
352 -endif.
Line Hits Source