23d044971ec99db3793eccc54e6b9e588744dc42
[asterisk/asterisk.git] / res / res_stasis.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2012 - 2013, Digium, Inc.
5  *
6  * David M. Lee, II <dlee@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Stasis application support.
22  *
23  * \author David M. Lee, II <dlee@digium.com>
24  *
25  * <code>res_stasis.so</code> brings together the various components of the
26  * Stasis application infrastructure.
27  *
28  * First, there's the Stasis application handler, stasis_app_exec(). This is
29  * called by <code>app_stasis.so</code> to give control of a channel to the
30  * Stasis application code from the dialplan.
31  *
32  * While a channel is in stasis_app_exec(), it has a \ref stasis_app_control
33  * object, which may be used to control the channel.
34  *
35  * To control the channel, commands may be sent to channel using
36  * stasis_app_send_command() and stasis_app_send_async_command().
37  *
38  * Alongside this, applications may be registered/unregistered using
39  * stasis_app_register()/stasis_app_unregister(). While a channel is in Stasis,
40  * events received on the channel's topic are converted to JSON and forwarded to
41  * the \ref stasis_app_cb. The application may also subscribe to the channel to
42  * continue to receive messages even after the channel has left Stasis, but it
43  * will not be able to control it.
44  *
45  * Given all the stuff that comes together in this module, it's been broken up
46  * into several pieces that are in <code>res/stasis/</code> and compiled into
47  * <code>res_stasis.so</code>.
48  */
49
50 /*** MODULEINFO
51         <depend>res_stasis_json_events</depend>
52         <support_level>core</support_level>
53  ***/
54
55 #include "asterisk.h"
56
57 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
58
59 #include "asterisk/astobj2.h"
60 #include "asterisk/callerid.h"
61 #include "asterisk/module.h"
62 #include "asterisk/stasis_app_impl.h"
63 #include "asterisk/stasis_channels.h"
64 #include "asterisk/stasis_message_router.h"
65 #include "asterisk/strings.h"
66 #include "stasis/app.h"
67 #include "stasis/control.h"
68 #include "stasis_json/resource_events.h"
69
70 /*! Time to wait for a frame in the application */
71 #define MAX_WAIT_MS 200
72
73 /*!
74  * \brief Number of buckets for the Stasis application hash table.  Remember to
75  * keep it a prime number!
76  */
77 #define APPS_NUM_BUCKETS 127
78
79 /*!
80  * \brief Number of buckets for the Stasis application hash table.  Remember to
81  * keep it a prime number!
82  */
83 #define CONTROLS_NUM_BUCKETS 127
84
85 /*!
86  * \brief Stasis application container.
87  */
88 struct ao2_container *apps_registry;
89
90 struct ao2_container *app_controls;
91
92 /*! \brief Message router for the channel caching topic */
93 struct stasis_message_router *channel_router;
94
95 /*! AO2 hash function for \ref app */
96 static int app_hash(const void *obj, const int flags)
97 {
98         const struct app *app = obj;
99         const char *name = flags & OBJ_KEY ? obj : app_name(app);
100
101         return ast_str_hash(name);
102 }
103
104 /*! AO2 comparison function for \ref app */
105 static int app_compare(void *lhs, void *rhs, int flags)
106 {
107         const struct app *lhs_app = lhs;
108         const struct app *rhs_app = rhs;
109         const char *lhs_name = app_name(lhs_app);
110         const char *rhs_name = flags & OBJ_KEY ? rhs : app_name(rhs_app);
111
112         if (strcmp(lhs_name, rhs_name) == 0) {
113                 return CMP_MATCH | CMP_STOP;
114         } else {
115                 return 0;
116         }
117 }
118
119 /*! AO2 hash function for \ref stasis_app_control */
120 static int control_hash(const void *obj, const int flags)
121 {
122         const struct stasis_app_control *control = obj;
123         const char *id = flags & OBJ_KEY ?
124                 obj : stasis_app_control_get_channel_id(control);
125
126         return ast_str_hash(id);
127 }
128
129 /*! AO2 comparison function for \ref stasis_app_control */
130 static int control_compare(void *lhs, void *rhs, int flags)
131 {
132         const struct stasis_app_control *lhs_control = lhs;
133         const struct stasis_app_control *rhs_control = rhs;
134         const char *lhs_id = stasis_app_control_get_channel_id(lhs_control);
135         const char *rhs_id = flags & OBJ_KEY ?
136                 rhs : stasis_app_control_get_channel_id(rhs_control);
137
138         if (strcmp(lhs_id, rhs_id) == 0) {
139                 return CMP_MATCH | CMP_STOP;
140         } else {
141                 return 0;
142         }
143 }
144
145 struct stasis_app_control *stasis_app_control_find_by_channel(
146         const struct ast_channel *chan)
147 {
148         if (chan == NULL) {
149                 return NULL;
150         }
151
152         return stasis_app_control_find_by_channel_id(
153                 ast_channel_uniqueid(chan));
154 }
155
156 struct stasis_app_control *stasis_app_control_find_by_channel_id(
157         const char *channel_id)
158 {
159         return ao2_find(app_controls, channel_id, OBJ_KEY);
160 }
161
162 /*! \brief Typedef for blob handler callbacks */
163 typedef struct ast_json *(*channel_blob_handler_cb)(struct ast_channel_blob *);
164
165 static int app_watching_channel_cb(void *obj, void *arg, int flags)
166 {
167         struct app *app = obj;
168         char *uniqueid = arg;
169
170         return app_is_watching_channel(app, uniqueid) ? CMP_MATCH : 0;
171 }
172
173 static struct ao2_container *get_watching_apps(const char *uniqueid)
174 {
175         struct ao2_container *watching_apps;
176         char *uniqueid_dup;
177         RAII_VAR(struct ao2_iterator *,watching_apps_iter, NULL, ao2_iterator_destroy);
178         ast_assert(uniqueid != NULL);
179
180         uniqueid_dup = ast_strdupa(uniqueid);
181
182         watching_apps_iter = ao2_callback(apps_registry, OBJ_MULTIPLE, app_watching_channel_cb, uniqueid_dup);
183         watching_apps = watching_apps_iter->c;
184
185         if (!ao2_container_count(watching_apps)) {
186                 return NULL;
187         }
188
189         ao2_ref(watching_apps, +1);
190         return watching_apps_iter->c;
191 }
192
193 /*! \brief Typedef for callbacks that get called on channel snapshot updates */
194 typedef struct ast_json *(*channel_snapshot_monitor)(
195         struct ast_channel_snapshot *old_snapshot,
196         struct ast_channel_snapshot *new_snapshot);
197
198 /*! \brief Handle channel state changes */
199 static struct ast_json *channel_state(
200         struct ast_channel_snapshot *old_snapshot,
201         struct ast_channel_snapshot *new_snapshot)
202 {
203         RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
204         struct ast_channel_snapshot *snapshot = new_snapshot ? new_snapshot : old_snapshot;
205
206         if (!old_snapshot) {
207                 return stasis_json_event_channel_created_create(snapshot);
208         } else if (!new_snapshot) {
209                 json = ast_json_pack("{s: i, s: s}",
210                         "cause", snapshot->hangupcause,
211                         "cause_txt", ast_cause2str(snapshot->hangupcause));
212                 if (!json) {
213                         return NULL;
214                 }
215                 return stasis_json_event_channel_destroyed_create(snapshot, json);
216         } else if (old_snapshot->state != new_snapshot->state) {
217                 return stasis_json_event_channel_state_change_create(snapshot);
218         }
219
220         return NULL;
221 }
222
223 static struct ast_json *channel_dialplan(
224         struct ast_channel_snapshot *old_snapshot,
225         struct ast_channel_snapshot *new_snapshot)
226 {
227         RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
228
229         /* No Newexten event on cache clear */
230         if (!new_snapshot) {
231                 return NULL;
232         }
233
234         /* Empty application is not valid for a Newexten event */
235         if (ast_strlen_zero(new_snapshot->appl)) {
236                 return NULL;
237         }
238
239         if (old_snapshot && ast_channel_snapshot_cep_equal(old_snapshot, new_snapshot)) {
240                 return NULL;
241         }
242
243         json = ast_json_pack("{s: s, s: s}",
244                 "application", new_snapshot->appl,
245                 "application_data", new_snapshot->data);
246         if (!json) {
247                 return NULL;
248         }
249
250         return stasis_json_event_channel_dialplan_create(new_snapshot, json);
251 }
252
253 static struct ast_json *channel_callerid(
254         struct ast_channel_snapshot *old_snapshot,
255         struct ast_channel_snapshot *new_snapshot)
256 {
257         RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
258
259         /* No NewCallerid event on cache clear or first event */
260         if (!old_snapshot || !new_snapshot) {
261                 return NULL;
262         }
263
264         if (ast_channel_snapshot_caller_id_equal(old_snapshot, new_snapshot)) {
265                 return NULL;
266         }
267
268         json = ast_json_pack("{s: i, s: s}",
269                 "caller_presentation", new_snapshot->caller_pres,
270                 "caller_presentation_txt", ast_describe_caller_presentation(new_snapshot->caller_pres));
271         if (!json) {
272                 return NULL;
273         }
274
275         return stasis_json_event_channel_caller_id_create(new_snapshot, json);
276 }
277
278 static struct ast_json *channel_snapshot(
279         struct ast_channel_snapshot *old_snapshot,
280         struct ast_channel_snapshot *new_snapshot)
281 {
282         if (!new_snapshot) {
283                 return NULL;
284         }
285
286         return stasis_json_event_channel_snapshot_create(new_snapshot);
287 }
288
289 channel_snapshot_monitor channel_monitors[] = {
290         channel_snapshot,
291         channel_state,
292         channel_dialplan,
293         channel_callerid
294 };
295
296 static int app_send_cb(void *obj, void *arg, int flags)
297 {
298         struct app *app = obj;
299         struct ast_json *msg = arg;
300
301         app_send(app, msg);
302         return 0;
303 }
304
305 static void sub_snapshot_handler(void *data,
306                 struct stasis_subscription *sub,
307                 struct stasis_topic *topic,
308                 struct stasis_message *message)
309 {
310         RAII_VAR(struct ao2_container *, watching_apps, NULL, ao2_cleanup);
311         struct stasis_cache_update *update = stasis_message_data(message);
312         struct ast_channel_snapshot *new_snapshot = stasis_message_data(update->new_snapshot);
313         struct ast_channel_snapshot *old_snapshot = stasis_message_data(update->old_snapshot);
314         int i;
315
316         watching_apps = get_watching_apps(new_snapshot ? new_snapshot->uniqueid : old_snapshot->uniqueid);
317         if (!watching_apps) {
318                 return;
319         }
320
321         for (i = 0; i < ARRAY_LEN(channel_monitors); ++i) {
322                 RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
323
324                 msg = channel_monitors[i](old_snapshot, new_snapshot);
325                 if (msg) {
326                         ao2_callback(watching_apps, OBJ_NODATA, app_send_cb, msg);
327                 }
328         }
329 }
330
331 static void distribute_message(struct ao2_container *apps, struct ast_json *msg)
332 {
333         ao2_callback(apps, OBJ_NODATA, app_send_cb, msg);
334 }
335
336 static void generic_blob_handler(struct ast_channel_blob *obj, channel_blob_handler_cb handler_cb)
337 {
338         RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
339         RAII_VAR(struct ao2_container *, watching_apps, NULL, ao2_cleanup);
340
341         if (!obj->snapshot) {
342                 return;
343         }
344
345         watching_apps = get_watching_apps(obj->snapshot->uniqueid);
346         if (!watching_apps) {
347                 return;
348         }
349
350         msg = handler_cb(obj);
351         if (!msg) {
352                 return;
353         }
354
355         distribute_message(watching_apps, msg);
356 }
357
358 /*!
359  * \brief In addition to running ao2_cleanup(), this function also removes the
360  * object from the app_controls() container.
361  */
362 static void control_unlink(struct stasis_app_control *control)
363 {
364         if (!control) {
365                 return;
366         }
367
368         ao2_unlink_flags(app_controls, control,
369                 OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
370         ao2_cleanup(control);
371 }
372
373 int app_send_start_msg(struct app *app, struct ast_channel *chan,
374         int argc, char *argv[])
375 {
376         RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
377         RAII_VAR(struct ast_json *, blob, NULL, ast_json_unref);
378         RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
379
380         struct ast_json *json_args;
381         int i;
382
383         ast_assert(chan != NULL);
384
385         /* Set channel info */
386         snapshot = ast_channel_snapshot_create(chan);
387         if (!snapshot) {
388                 return -1;
389         }
390
391         blob = ast_json_pack("{s: []}", "args");
392         if (!blob) {
393                 return -1;
394         }
395
396         /* Append arguments to args array */
397         json_args = ast_json_object_get(blob, "args");
398         ast_assert(json_args != NULL);
399         for (i = 0; i < argc; ++i) {
400                 int r = ast_json_array_append(json_args,
401                                               ast_json_string_create(argv[i]));
402                 if (r != 0) {
403                         ast_log(LOG_ERROR, "Error appending start message\n");
404                         return -1;
405                 }
406         }
407
408         msg = stasis_json_event_stasis_start_create(snapshot, blob);
409         if (!msg) {
410                 return -1;
411         }
412
413         app_send(app, msg);
414         return 0;
415 }
416
417 int app_send_end_msg(struct app *app, struct ast_channel *chan)
418 {
419         RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
420         RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
421
422         ast_assert(chan != NULL);
423
424         /* Set channel info */
425         snapshot = ast_channel_snapshot_create(chan);
426         if (snapshot == NULL) {
427                 return -1;
428         }
429
430         msg = stasis_json_event_stasis_end_create(snapshot);
431         if (!msg) {
432                 return -1;
433         }
434
435         app_send(app, msg);
436         return 0;
437 }
438
439 /*! /brief Stasis dialplan application callback */
440 int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
441                     char *argv[])
442 {
443         SCOPED_MODULE_USE(ast_module_info->self);
444
445         RAII_VAR(struct app *, app, NULL, ao2_cleanup);
446         RAII_VAR(struct stasis_app_control *, control, NULL, control_unlink);
447         int res = 0;
448
449         ast_assert(chan != NULL);
450
451         app = ao2_find(apps_registry, app_name, OBJ_KEY);
452         if (!app) {
453                 ast_log(LOG_ERROR,
454                         "Stasis app '%s' not registered\n", app_name);
455                 return -1;
456         }
457
458         control = control_create(chan);
459         if (!control) {
460                 ast_log(LOG_ERROR, "Allocated failed\n");
461                 return -1;
462         }
463         ao2_link(app_controls, control);
464
465         res = app_send_start_msg(app, chan, argc, argv);
466         if (res != 0) {
467                 ast_log(LOG_ERROR,
468                         "Error sending start message to %s\n", app_name);
469                 return res;
470         }
471
472         if (app_add_channel(app, chan)) {
473                 ast_log(LOG_ERROR, "Error adding listener for channel %s to app %s\n", ast_channel_name(chan), app_name);
474                 return -1;
475         }
476
477         while (!control_is_done(control)) {
478                 RAII_VAR(struct ast_frame *, f, NULL, ast_frame_dtor);
479                 int r;
480                 int command_count;
481
482                 r = ast_waitfor(chan, MAX_WAIT_MS);
483
484                 if (r < 0) {
485                         ast_debug(3, "%s: Poll error\n",
486                                   ast_channel_uniqueid(chan));
487                         break;
488                 }
489
490                 command_count = control_dispatch_all(control, chan);
491
492                 if (command_count > 0 && ast_channel_fdno(chan) == -1) {
493                         /* Command drained the channel; wait for next frame */
494                         continue;
495                 }
496
497                 if (r == 0) {
498                         /* Timeout */
499                         continue;
500                 }
501
502                 f = ast_read(chan);
503                 if (!f) {
504                         ast_debug(3,
505                                 "%s: No more frames. Must be done, I guess.\n",
506                                 ast_channel_uniqueid(chan));
507                         break;
508                 }
509
510                 switch (f->frametype) {
511                 case AST_FRAME_CONTROL:
512                         if (f->subclass.integer == AST_CONTROL_HANGUP) {
513                                 /* Continue on in the dialplan */
514                                 ast_debug(3, "%s: Hangup\n",
515                                         ast_channel_uniqueid(chan));
516                                 control_continue(control);
517                         }
518                         break;
519                 default:
520                         /* Not handled; discard */
521                         break;
522                 }
523         }
524
525         app_remove_channel(app, chan);
526         res = app_send_end_msg(app, chan);
527         if (res != 0) {
528                 ast_log(LOG_ERROR,
529                         "Error sending end message to %s\n", app_name);
530                 return res;
531         }
532
533         return res;
534 }
535
536 int stasis_app_send(const char *app_name, struct ast_json *message)
537 {
538         RAII_VAR(struct app *, app, NULL, ao2_cleanup);
539
540         app = ao2_find(apps_registry, app_name, OBJ_KEY);
541
542         if (!app) {
543                 /* XXX We can do a better job handling late binding, queueing up
544                  * the call for a few seconds to wait for the app to register.
545                  */
546                 ast_log(LOG_WARNING,
547                         "Stasis app '%s' not registered\n", app_name);
548                 return -1;
549         }
550
551         app_send(app, message);
552         return 0;
553 }
554
555 int stasis_app_register(const char *app_name, stasis_app_cb handler, void *data)
556 {
557         RAII_VAR(struct app *, app, NULL, ao2_cleanup);
558
559         SCOPED_LOCK(apps_lock, apps_registry, ao2_lock, ao2_unlock);
560
561         app = ao2_find(apps_registry, app_name, OBJ_KEY | OBJ_NOLOCK);
562
563         if (app) {
564                 RAII_VAR(struct ast_json *, blob, NULL, ast_json_unref);
565                 RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
566
567                 blob = ast_json_pack("{s: s}", "application", app_name);
568                 if (blob) {
569                         msg = stasis_json_event_application_replaced_create(blob);
570                         if (msg) {
571                                 app_send(app, msg);
572                         }
573                 }
574
575                 app_update(app, handler, data);
576         } else {
577                 app = app_create(app_name, handler, data);
578                 if (app) {
579                         ao2_link_flags(apps_registry, app, OBJ_NOLOCK);
580                 } else {
581                         return -1;
582                 }
583         }
584
585         return 0;
586 }
587
588 void stasis_app_unregister(const char *app_name)
589 {
590         if (app_name) {
591                 ao2_cleanup(ao2_find(
592                                 apps_registry, app_name, OBJ_KEY | OBJ_UNLINK));
593         }
594 }
595
596 static struct ast_json *handle_blob_dtmf(struct ast_channel_blob *obj)
597 {
598         RAII_VAR(struct ast_json *, extra, NULL, ast_json_unref);
599         RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
600         const char *direction;
601
602         /* To simplify events, we'll only generate on receive */
603         direction = ast_json_string_get(
604                 ast_json_object_get(obj->blob, "direction"));
605
606         if (strcmp("Received", direction) != 0) {
607                 return NULL;
608         }
609
610         extra = ast_json_pack(
611                 "{s: o}",
612                 "digit", ast_json_ref(ast_json_object_get(obj->blob, "digit")));
613         if (!extra) {
614                 return NULL;
615         }
616
617         return stasis_json_event_channel_dtmf_received_create(obj->snapshot, extra);
618 }
619
620 /* To simplify events, we'll only generate on DTMF end (dtmf_end type) */
621 static void sub_dtmf_handler(void *data,
622                 struct stasis_subscription *sub,
623                 struct stasis_topic *topic,
624                 struct stasis_message *message)
625 {
626         struct ast_channel_blob *obj = stasis_message_data(message);
627         generic_blob_handler(obj, handle_blob_dtmf);
628 }
629
630 static struct ast_json *handle_blob_userevent(struct ast_channel_blob *obj)
631 {
632         return stasis_json_event_channel_userevent_create(obj->snapshot, obj->blob);
633 }
634
635 static void sub_userevent_handler(void *data,
636                 struct stasis_subscription *sub,
637                 struct stasis_topic *topic,
638                 struct stasis_message *message)
639 {
640         struct ast_channel_blob *obj = stasis_message_data(message);
641         generic_blob_handler(obj, handle_blob_userevent);
642 }
643
644 static struct ast_json *handle_blob_hangup_request(struct ast_channel_blob *obj)
645 {
646         return stasis_json_event_channel_hangup_request_create(obj->snapshot, obj->blob);
647 }
648
649 static void sub_hangup_request_handler(void *data,
650                 struct stasis_subscription *sub,
651                 struct stasis_topic *topic,
652                 struct stasis_message *message)
653 {
654         struct ast_channel_blob *obj = stasis_message_data(message);
655         generic_blob_handler(obj, handle_blob_hangup_request);
656 }
657
658 static struct ast_json *handle_blob_varset(struct ast_channel_blob *obj)
659 {
660         return stasis_json_event_channel_varset_create(obj->snapshot, obj->blob);
661 }
662
663 static void sub_varset_handler(void *data,
664                 struct stasis_subscription *sub,
665                 struct stasis_topic *topic,
666                 struct stasis_message *message)
667 {
668         struct ast_channel_blob *obj = stasis_message_data(message);
669         generic_blob_handler(obj, handle_blob_varset);
670 }
671
672 void stasis_app_ref(void)
673 {
674         ast_module_ref(ast_module_info->self);
675 }
676
677 void stasis_app_unref(void)
678 {
679         ast_module_unref(ast_module_info->self);
680 }
681
682 static int load_module(void)
683 {
684         int r = 0;
685
686         apps_registry =
687                 ao2_container_alloc(APPS_NUM_BUCKETS, app_hash, app_compare);
688         if (apps_registry == NULL) {
689                 return AST_MODULE_LOAD_FAILURE;
690         }
691
692         app_controls = ao2_container_alloc(CONTROLS_NUM_BUCKETS,
693                                              control_hash, control_compare);
694         if (app_controls == NULL) {
695                 return AST_MODULE_LOAD_FAILURE;
696         }
697
698         channel_router = stasis_message_router_create(stasis_caching_get_topic(ast_channel_topic_all_cached()));
699         if (!channel_router) {
700                 return AST_MODULE_LOAD_FAILURE;
701         }
702
703         r |= stasis_message_router_add(channel_router, stasis_cache_update_type(), sub_snapshot_handler, NULL);
704         r |= stasis_message_router_add(channel_router, ast_channel_user_event_type(), sub_userevent_handler, NULL);
705         r |= stasis_message_router_add(channel_router, ast_channel_varset_type(), sub_varset_handler, NULL);
706         r |= stasis_message_router_add(channel_router, ast_channel_dtmf_begin_type(), sub_dtmf_handler, NULL);
707         r |= stasis_message_router_add(channel_router, ast_channel_hangup_request_type(), sub_hangup_request_handler, NULL);
708         if (r) {
709                 return AST_MODULE_LOAD_FAILURE;
710         }
711
712         return AST_MODULE_LOAD_SUCCESS;
713 }
714
715 static int unload_module(void)
716 {
717         int r = 0;
718
719         stasis_message_router_unsubscribe_and_join(channel_router);
720         channel_router = NULL;
721
722         ao2_cleanup(apps_registry);
723         apps_registry = NULL;
724
725         ao2_cleanup(app_controls);
726         app_controls = NULL;
727
728         return r;
729 }
730
731 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS,
732                 "Stasis application support",
733                 .load = load_module,
734                 .unload = unload_module);