From a2a53cc306ea5fec65daf3630716a7c6ee13adad Mon Sep 17 00:00:00 2001 From: "David M. Lee" Date: Mon, 8 Apr 2013 13:27:45 +0000 Subject: [PATCH] Stasis application WebSocket support This is the API that binds the Stasis dialplan application to external Stasis applications. It also adds the beginnings of WebSocket application support. This module registers a dialplan function named Stasis, which is used to put a channel into the named Stasis app. As a channel enters and leaves the Stasis diaplan application, the Stasis app receives a 'stasis-start' and 'stasis-end' events. Stasis apps register themselves using the stasis_app_register and stasis_app_unregister functions. Messages are sent to an application using stasis_app_send. Finally, Stasis apps control channels through the use of the stasis_app_control object, and the family of stasis_app_control_* functions. Other changes along for the ride are: * An ast_frame_dtor function that's RAII_VAR safe * Some common JSON encoders for name/number, timeval, and context/extension/priority Review: https://reviewboard.asterisk.org/r/2361/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@384879 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- apps/Makefile | 1 + apps/app_stasis.c | 555 ++++++++++++++++++++++++++++++++++ apps/app_stasis.exports.in | 6 + apps/stasis_json.c | 77 +++++ include/asterisk/app_stasis.h | 138 +++++++++ include/asterisk/frame.h | 74 ++--- include/asterisk/json.h | 44 +++ include/asterisk/localtime.h | 5 + main/frame.c | 7 + main/json.c | 34 +++ res/res_stasis_websocket.c | 326 ++++++++++++++++++++ tests/test_abstract_jb.c | 25 +- tests/test_app_stasis.c | 194 ++++++++++++ tests/test_json.c | 102 +++++++ 14 files changed, 1536 insertions(+), 52 deletions(-) create mode 100644 apps/app_stasis.c create mode 100644 apps/app_stasis.exports.in create mode 100644 apps/stasis_json.c create mode 100644 include/asterisk/app_stasis.h create mode 100644 res/res_stasis_websocket.c create mode 100644 tests/test_app_stasis.c diff --git a/apps/Makefile b/apps/Makefile index e3762d7031..150f5a358a 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -38,3 +38,4 @@ ifneq ($(findstring $(OSARCH), mingw32 cygwin ),) LIBS+= -lres_smdi.so endif +app_stasis.so: stasis_json.o diff --git a/apps/app_stasis.c b/apps/app_stasis.c new file mode 100644 index 0000000000..769c915210 --- /dev/null +++ b/apps/app_stasis.c @@ -0,0 +1,555 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2012 - 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Stasis dialplan application. + * + * \author David M. Lee, II + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/app.h" +#include "asterisk/app_stasis.h" +#include "asterisk/astobj2.h" +#include "asterisk/channel.h" +#include "asterisk/module.h" +#include "asterisk/stasis.h" +#include "asterisk/strings.h" + +/*** DOCUMENTATION + + Invoke an external Stasis application. + + + Name of the application to invoke. + + + Optional comma-delimited arguments for the application invocation. + + + + + Invoke a Stasis application. + + + + ***/ + +/*! \brief Maximum number of arguments for the Stasis dialplan application */ +#define MAX_ARGS 128 + +/*! \brief Dialplan application name */ +static const char *stasis = "Stasis"; + +/*! + * \brief Number of buckets for the Stasis application hash table. Remember to + * keep it a prime number! + */ +#define APPS_NUM_BUCKETS 127 + +/*! + * \brief Number of buckets for the Stasis application hash table. Remember to + * keep it a prime number! + */ +#define CONTROLS_NUM_BUCKETS 127 + +/*! + * \brief Stasis application container. Please call apps_registry() instead of + * directly accessing. + */ +struct ao2_container *__apps_registry; + +struct ao2_container *__app_controls; + +/*! Ref-counting accessor for the stasis applications container */ +static struct ao2_container *apps_registry(void) +{ + ao2_ref(__apps_registry, +1); + return __apps_registry; +} + +static struct ao2_container *app_controls(void) +{ + ao2_ref(__app_controls, +1); + return __app_controls; +} + +struct app { + /*! Callback function for this application. */ + stasis_app_cb handler; + /*! Opaque data to hand to callback function. */ + void *data; + /*! Name of the Stasis application */ + char name[]; +}; + +static void app_dtor(void *obj) +{ + struct app *app = obj; + + ao2_cleanup(app->data); + app->data = NULL; +} + +/*! Constructor for \ref app. */ +static struct app *app_create(const char *name, stasis_app_cb handler, void *data) +{ + struct app *app; + size_t size; + + ast_assert(name != NULL); + ast_assert(handler != NULL); + + size = sizeof(*app) + strlen(name) + 1; + app = ao2_alloc_options(size, app_dtor, AO2_ALLOC_OPT_LOCK_MUTEX); + + if (!app) { + return NULL; + } + + strncpy(app->name, name, size - sizeof(*app)); + app->handler = handler; + ao2_ref(data, +1); + app->data = data; + + return app; +} + +/*! AO2 hash function for \ref app */ +static int app_hash(const void *obj, const int flags) +{ + const struct app *app = obj; + const char *name = flags & OBJ_KEY ? obj : app->name; + + return ast_str_hash(name); +} + +/*! AO2 comparison function for \ref app */ +static int app_compare(void *lhs, void *rhs, int flags) +{ + const struct app *lhs_app = lhs; + const struct app *rhs_app = rhs; + const char *rhs_name = flags & OBJ_KEY ? rhs : rhs_app->name; + + if (strcmp(lhs_app->name, rhs_name) == 0) { + return CMP_MATCH | CMP_STOP; + } else { + return 0; + } +} + +/*! + * \brief Send a message to the given application. + * \param app App to send the message to. + * \param message Message to send. + */ +static void app_send(struct app *app, struct ast_json *message) +{ + app->handler(app->data, app->name, message); +} + +struct stasis_app_control { + /*! + * When set, /c app_stasis should exit and continue in the dialplan. + */ + int continue_to_dialplan:1; + /*! Uniqueid of the associated channel */ + char channel_uniqueid[]; +}; + +static struct stasis_app_control *control_create(const char *uniqueid) +{ + struct stasis_app_control *control; + size_t size; + + size = sizeof(*control) + strlen(uniqueid) + 1; + control = ao2_alloc(size, NULL); + if (!control) { + return NULL; + } + + strncpy(control->channel_uniqueid, uniqueid, size - sizeof(*control)); + + return control; +} + +struct stasis_app_control *stasis_app_control_find_by_channel( + const struct ast_channel *chan) +{ + RAII_VAR(struct ao2_container *, controls, NULL, ao2_cleanup); + if (chan == NULL) { + return NULL; + } + + controls = app_controls(); + return ao2_find(controls, ast_channel_uniqueid(chan), OBJ_KEY); +} + +/*! + * \brief Test the \c continue_to_dialplan bit for the given \a app. + * + * The bit is also reset for the next call. + * + * \param app Application to check the \c continue_to_dialplan bit. + * \return Zero to remain in \c Stasis + * \return Non-zero to continue in the dialplan + */ +static int control_continue_test_and_reset(struct stasis_app_control *control) +{ + int r; + SCOPED_AO2LOCK(lock, control); + + r = control->continue_to_dialplan; + control->continue_to_dialplan = 0; + return r; +} + +void stasis_app_control_continue(struct stasis_app_control *control) +{ + SCOPED_AO2LOCK(lock, control); + control->continue_to_dialplan = 1; +} + +static struct ast_json *app_event_create( + const char *event_name, + const struct ast_channel_snapshot *snapshot, + const struct ast_json *extra_info) +{ + RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, event, NULL, ast_json_unref); + + if (extra_info) { + event = ast_json_deep_copy(extra_info); + } else { + event = ast_json_object_create(); + } + + if (snapshot) { + int ret; + + /* Mustn't already have a channel field */ + ast_assert(ast_json_object_get(event, "channel") == NULL); + + ret = ast_json_object_set( + event, + "channel", ast_channel_snapshot_to_json(snapshot)); + if (ret != 0) { + return NULL; + } + } + + message = ast_json_pack("{s: o}", event_name, ast_json_ref(event)); + + return ast_json_ref(message); +} + +static int send_start_msg(struct app *app, struct ast_channel *chan, + int argc, char *argv[]) +{ + RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref); + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + + struct ast_json *json_args; + int i; + + ast_assert(chan != NULL); + + /* Set channel info */ + snapshot = ast_channel_snapshot_create(chan); + if (!snapshot) { + return -1; + } + + msg = ast_json_pack("{s: {s: [], s: o}}", + "stasis-start", + "args", + "channel", ast_channel_snapshot_to_json(snapshot)); + + if (!msg) { + return -1; + } + + /* Append arguments to args array */ + json_args = ast_json_object_get( + ast_json_object_get(msg, "stasis-start"), + "args"); + ast_assert(json_args != NULL); + for (i = 0; i < argc; ++i) { + int r = ast_json_array_append(json_args, + ast_json_string_create(argv[i])); + if (r != 0) { + ast_log(LOG_ERROR, "Error appending start message\n"); + return -1; + } + } + + app_send(app, msg); + return 0; +} + +static int send_end_msg(struct app *app, struct ast_channel *chan) +{ + RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref); + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + + ast_assert(chan != NULL); + + /* Set channel info */ + snapshot = ast_channel_snapshot_create(chan); + if (snapshot == NULL) { + return -1; + } + msg = app_event_create("stasis-end", snapshot, NULL); + if (!msg) { + return -1; + } + + app_send(app, msg); + return 0; +} + +static void sub_handler(void *data, struct stasis_subscription *sub, + struct stasis_topic *topic, + struct stasis_message *message) +{ + struct app *app = data; + if (ast_channel_snapshot_type() == stasis_message_type(message)) { + RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref); + struct ast_channel_snapshot *snapshot = + stasis_message_data(message); + + msg = app_event_create("channel-state-change", snapshot, NULL); + if (!msg) { + return; + } + app_send(app, msg); + } + if (stasis_subscription_final_message(sub, message)) { + ao2_cleanup(data); + } +} + +/*! + * \brief In addition to running ao2_cleanup(), this function also removes the + * object from the app_controls() container. + */ +static void control_unlink(struct stasis_app_control *control) +{ + RAII_VAR(struct ao2_container *, controls, NULL, ao2_cleanup); + + if (!control) { + return; + } + + controls = app_controls(); + ao2_unlink_flags(controls, control, OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA); + ao2_cleanup(control); +} + +/*! /brief Stasis dialplan application callback */ +static int app_stasis_exec(struct ast_channel *chan, const char *data) +{ + RAII_VAR(struct ao2_container *, apps, apps_registry(), ao2_cleanup); + RAII_VAR(struct app *, app, NULL, ao2_cleanup); + RAII_VAR(struct stasis_app_control *, control, NULL, control_unlink); + RAII_VAR(struct stasis_subscription *, subscription, NULL, stasis_unsubscribe); + int res = 0; + char *parse = NULL; + int hungup = 0; + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(app_name); + AST_APP_ARG(app_argv)[MAX_ARGS]; + ); + + ast_assert(chan != NULL); + ast_assert(data != NULL); + + /* parse the arguments */ + parse = ast_strdupa(data); + AST_STANDARD_APP_ARGS(args, parse); + + if (args.argc < 1) { + ast_log(LOG_WARNING, "Stasis app_name argument missing\n"); + return -1; + } + + app = ao2_find(apps, args.app_name, OBJ_KEY); + if (!app) { + ast_log(LOG_ERROR, "Stasis app '%s' not registered\n", args.app_name); + return -1; + } + + { + RAII_VAR(struct ao2_container *, controls, NULL, ao2_cleanup); + + controls = app_controls(); + control = control_create(ast_channel_uniqueid(chan)); + if (!control) { + ast_log(LOG_ERROR, "Allocated failed\n"); + return -1; + } + ao2_link(controls, control); + } + + subscription = stasis_subscribe(ast_channel_topic(chan), sub_handler, app); + if (subscription == NULL) { + ast_log(LOG_ERROR, "Error subscribing app %s to channel %s\n", args.app_name, ast_channel_name(chan)); + return -1; + } + ao2_ref(app, +1); /* subscription now has a reference */ + + res = send_start_msg(app, chan, args.argc - 1, args.app_argv); + if (res != 0) { + ast_log(LOG_ERROR, "Error sending start message to %s\n", args.app_name); + return res; + } + + while (!hungup && !control_continue_test_and_reset(control) && ast_waitfor(chan, -1) > -1) { + RAII_VAR(struct ast_frame *, f, ast_read(chan), ast_frame_dtor); + if (!f) { + ast_debug(3, "%s: No more frames. Must be done, I guess.\n", ast_channel_uniqueid(chan)); + break; + } + + switch (f->frametype) { + case AST_FRAME_CONTROL: + if (f->subclass.integer == AST_CONTROL_HANGUP) { + ast_debug(3, "%s: Received hangup\n", ast_channel_uniqueid(chan)); + hungup = 1; + } + break; + default: + /* Not handled; discard */ + break; + } + } + + res = send_end_msg(app, chan); + if (res != 0) { + ast_log(LOG_ERROR, "Error sending end message to %s\n", args.app_name); + return res; + } + + return res; +} + +int stasis_app_send(const char *app_name, struct ast_json *message) +{ + RAII_VAR(struct ao2_container *, apps, apps_registry(), ao2_cleanup); + RAII_VAR(struct app *, app, NULL, ao2_cleanup); + + app = ao2_find(apps, app_name, OBJ_KEY); + + if (!app) { + /* XXX We can do a better job handling late binding, queueing up the call for a few seconds + * to wait for the app to register. + */ + ast_log(LOG_WARNING, "Stasis app '%s' not registered\n", app_name); + return -1; + } + + app_send(app, message); + return 0; +} + +int stasis_app_register(const char *app_name, stasis_app_cb handler, void *data) +{ + RAII_VAR(struct ao2_container *, apps, apps_registry(), ao2_cleanup); + RAII_VAR(struct app *, app, NULL, ao2_cleanup); + + SCOPED_LOCK(apps_lock, apps, ao2_lock, ao2_unlock); + + app = ao2_find(apps, app_name, OBJ_KEY | OBJ_NOLOCK); + + if (app) { + RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref); + SCOPED_LOCK(app_lock, app, ao2_lock, ao2_unlock); + + msg = app_event_create("application-replaced", NULL, NULL); + app->handler(app->data, app_name, msg); + + app->handler = handler; + ao2_cleanup(app->data); + ao2_ref(data, +1); + app->data = data; + } else { + app = app_create(app_name, handler, data); + if (app) { + ao2_link_flags(apps, app, OBJ_NOLOCK); + } else { + return -1; + } + } + + return 0; +} + +void stasis_app_unregister(const char *app_name) +{ + RAII_VAR(struct ao2_container *, apps, NULL, ao2_cleanup); + + if (app_name) { + apps = apps_registry(); + ao2_cleanup(ao2_find(apps, app_name, OBJ_KEY | OBJ_UNLINK)); + } +} + +static int load_module(void) +{ + int r = 0; + + __apps_registry = ao2_container_alloc(APPS_NUM_BUCKETS, app_hash, app_compare); + if (__apps_registry == NULL) { + return AST_MODULE_LOAD_FAILURE; + } + + __app_controls = ao2_container_alloc(CONTROLS_NUM_BUCKETS, app_hash, app_compare); + if (__app_controls == NULL) { + return AST_MODULE_LOAD_FAILURE; + } + + r |= ast_register_application_xml(stasis, app_stasis_exec); + return r; +} + +static int unload_module(void) +{ + int r = 0; + + ao2_cleanup(__apps_registry); + __apps_registry = NULL; + + ao2_cleanup(__app_controls); + __app_controls = NULL; + + r |= ast_unregister_application(stasis); + return r; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "Stasis dialplan application", + .load = load_module, + .unload = unload_module); diff --git a/apps/app_stasis.exports.in b/apps/app_stasis.exports.in new file mode 100644 index 0000000000..9616003233 --- /dev/null +++ b/apps/app_stasis.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXstasis_*; + local: + *; +}; diff --git a/apps/stasis_json.c b/apps/stasis_json.c new file mode 100644 index 0000000000..6b66b5d530 --- /dev/null +++ b/apps/stasis_json.c @@ -0,0 +1,77 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Stasis application JSON converters. + * + * \author David M. Lee, II + */ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/app_stasis.h" + +struct ast_json *ast_channel_snapshot_to_json(const struct ast_channel_snapshot *snapshot) +{ + RAII_VAR(struct ast_json *, json_chan, NULL, ast_json_unref); + int r = 0; + + if (snapshot == NULL) { + return NULL; + } + + json_chan = ast_json_object_create(); + if (!json_chan) { ast_log(LOG_ERROR, "Error creating channel json object\n"); return NULL; } + + r = ast_json_object_set(json_chan, "name", ast_json_string_create(snapshot->name)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "state", ast_json_string_create(ast_state2str(snapshot->state))); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "accountcode", ast_json_string_create(snapshot->accountcode)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "peeraccount", ast_json_string_create(snapshot->peeraccount)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "userfield", ast_json_string_create(snapshot->userfield)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "uniqueid", ast_json_string_create(snapshot->uniqueid)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "linkedid", ast_json_string_create(snapshot->linkedid)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "parkinglot", ast_json_string_create(snapshot->parkinglot)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "hangupsource", ast_json_string_create(snapshot->hangupsource)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "appl", ast_json_string_create(snapshot->appl)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "data", ast_json_string_create(snapshot->data)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "dialplan", ast_json_dialplan_cep(snapshot->context, snapshot->exten, snapshot->priority)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "caller", ast_json_name_number(snapshot->caller_name, snapshot->caller_number)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "connected", ast_json_name_number(snapshot->connected_name, snapshot->connected_number)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + r = ast_json_object_set(json_chan, "creationtime", ast_json_timeval(&snapshot->creationtime, NULL)); + if (r) { ast_log(LOG_ERROR, "Error adding attrib to channel json object\n"); return NULL; } + + return ast_json_ref(json_chan); +} + diff --git a/include/asterisk/app_stasis.h b/include/asterisk/app_stasis.h new file mode 100644 index 0000000000..921a35ee80 --- /dev/null +++ b/include/asterisk/app_stasis.h @@ -0,0 +1,138 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2012 - 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef _ASTERISK_APP_STASIS_H +#define _ASTERISK_APP_STASIS_H + +/*! \file + * + * \brief Stasis Application API. See \ref app_stasis "Stasis Application API" + * for detailed documentation. + * + * \author David M. Lee, II + * \since 12 + * + * \page app_stasis Stasis Application API + * + * This is the API that binds the Stasis dialplan application to external + * Stasis applications, such as \c res_stasis_websocket. + * + * This module registers a dialplan function named \c Stasis, which is used to + * put a channel into the named Stasis app. As a channel enters and leaves the + * Stasis diaplan applcation, the Stasis app receives a \c 'stasis-start' and \c + * 'stasis-end' events. + * + * Stasis apps register themselves using the \ref stasis_app_register and + * stasis_app_unregister functions. Messages are sent to an appliction using + * \ref stasis_app_send. + * + * Finally, Stasis apps control channels through the use of the \ref + * stasis_app_control object, and the family of \c stasis_app_control_* + * functions. + */ + +#include "asterisk/channel.h" +#include "asterisk/json.h" + +/*! @{ */ + +/*! + * \brief Callback for Stasis application handler. + * + * The message given to the handler is a borrowed copy. If you want to keep a + * reference to it, you should use \c ao2_ref() to keep it around. + * + * \param data Data ptr given when registered. + * \param app_name Name of the application being dispatched to. + * \param message Message to handle. (borrowed copy) + */ +typedef void (*stasis_app_cb)(void *data, const char *app_name, + struct ast_json *message); + +/*! + * \brief Register a new Stasis application. + * + * If an application is already registered with the given name, the old + * application is sent a 'replaced' message and unregistered. + * + * \param app_name Name of this application. + * \param handler Callback for application messages. + * \param data Data blob to pass to the callback. Must be AO2 managed. + * \return 0 for success + * \return -1 for error. + */ +int stasis_app_register(const char *app_name, stasis_app_cb handler, void *data); + +/*! + * \brief Unregister a Stasis application. + * \param app_name Name of the application to unregister. + */ +void stasis_app_unregister(const char *app_name); + +/*! + * \brief Send a message to the given Stasis application. + * + * The message given to the handler is a borrowed copy. If you want to keep a + * reference to it, you should use \c ao2_ref() to keep it around. + * + * \param app_name Name of the application to invoke. + * \param message Message to send (borrowed reference) + * \return 0 for success. + * \return -1 for error. + */ +int stasis_app_send(const char *app_name, struct ast_json *message); + +/*! @} */ + +/*! @{ */ + +/*! \brief Handler for controlling a channel that's in a Stasis application */ +struct stasis_app_control; + +/*! + * \brief Returns the handler for the given channel + * \param chan Channel to handle. + * \return NULL channel not in Stasis application + * \return Pointer to app_stasis handler. + */ +struct stasis_app_control *stasis_app_control_find_by_channel( + const struct ast_channel *chan); + +/*! + * \brief Exit \c app_stasis and continue execution in the dialplan. + * + * If the channel is no longer in \c app_stasis, this function does nothing. + * + * \param handler Handler for \c app_stasis + */ +void stasis_app_control_continue(struct stasis_app_control *handler); + +/*! @} */ + +/*! @{ */ + +/*! + * \brief Build a JSON object from a \ref ast_channel_snapshot. + * \return JSON object representing channel snapshot. + * \return \c NULL on error + */ +struct ast_json *ast_channel_snapshot_to_json(const struct ast_channel_snapshot *snapshot); + +/*! @} */ + +#endif /* _ASTERISK_APP_STASIS_H */ diff --git a/include/asterisk/frame.h b/include/asterisk/frame.h index 5e81b4e18c..abb0c2e28d 100644 --- a/include/asterisk/frame.h +++ b/include/asterisk/frame.h @@ -39,11 +39,11 @@ extern "C" { /*! * \page Def_Frame AST Multimedia and signalling frames * \section Def_AstFrame What is an ast_frame ? - * A frame of data read used to communicate between + * A frame of data read used to communicate between * between channels and applications. * Frames are divided into frame types and subclasses. * - * \par Frame types + * \par Frame types * \arg \b VOICE: Voice data, subclass is codec (AST_FORMAT_*) * \arg \b VIDEO: Video data, subclass is codec (AST_FORMAT_*) * \arg \b DTMF: A DTMF digit, subclass is the digit @@ -88,7 +88,7 @@ extern "C" { */ /*! - * \brief Frame types + * \brief Frame types * * \note It is important that the values of each frame type are never changed, * because it will break backwards compatability with older versions. @@ -113,11 +113,11 @@ enum ast_frame_type { AST_FRAME_IMAGE, /*! HTML Frame */ AST_FRAME_HTML, - /*! Comfort Noise frame (subclass is level of CNG in -dBov), + /*! Comfort Noise frame (subclass is level of CNG in -dBov), body may include zero or more 8-bit quantization coefficients */ AST_FRAME_CNG, /*! Modem-over-IP data streams */ - AST_FRAME_MODEM, + AST_FRAME_MODEM, /*! DTMF begin event, subclass is the digit */ AST_FRAME_DTMF_BEGIN, }; @@ -137,24 +137,24 @@ union ast_frame_subclass { */ struct ast_frame { /*! Kind of frame */ - enum ast_frame_type frametype; + enum ast_frame_type frametype; /*! Subclass, frame dependent */ union ast_frame_subclass subclass; /*! Length of data */ - int datalen; + int datalen; /*! Number of samples in this frame */ - int samples; + int samples; /*! Was the data malloc'd? i.e. should we free it when we discard the frame? */ - int mallocd; + int mallocd; /*! The number of bytes allocated for a malloc'd frame header */ size_t mallocd_hdr_len; /*! How many bytes exist _before_ "data" that can be used if needed */ - int offset; + int offset; /*! Optional source of frame for debugging */ - const char *src; + const char *src; /*! Pointer to actual data */ union { void *ptr; uint32_t uint32; char pad[8]; } data; - /*! Global delivery time */ + /*! Global delivery time */ struct timeval delivery; /*! For placing in a linked list */ AST_LIST_ENTRY(ast_frame) frame_list; @@ -197,7 +197,7 @@ extern struct ast_frame ast_null_frame; * RTP header information into the space provided by AST_FRIENDLY_OFFSET instead * of having to create a new buffer with the necessary space allocated. */ -#define AST_FRIENDLY_OFFSET 64 +#define AST_FRIENDLY_OFFSET 64 #define AST_MIN_OFFSET 32 /*! Make sure we keep at least this much handy */ /*! Need the header be free'd? */ @@ -353,10 +353,10 @@ struct ast_control_pvt_cause_code { #define AST_OPTION_FLAG_ANSWER 5 #define AST_OPTION_FLAG_WTF 6 -/*! Verify touchtones by muting audio transmission +/*! Verify touchtones by muting audio transmission * (and reception) and verify the tone is still present * Option data is a single signed char value 0 or 1 */ -#define AST_OPTION_TONE_VERIFY 1 +#define AST_OPTION_TONE_VERIFY 1 /*! Put a compatible channel into TDD (TTY for the hearing-impared) mode * Option data is a single signed char value 0 or 1 */ @@ -370,7 +370,7 @@ struct ast_control_pvt_cause_code { * Option data is a single signed char value 0 or 1 */ #define AST_OPTION_AUDIO_MODE 4 -/*! Set channel transmit gain +/*! Set channel transmit gain * Option data is a single signed char representing number of decibels (dB) * to set gain to (on top of any gain specified in channel driver) */ #define AST_OPTION_TXGAIN 5 @@ -380,7 +380,7 @@ struct ast_control_pvt_cause_code { * to set gain to (on top of any gain specified in channel driver) */ #define AST_OPTION_RXGAIN 6 -/* set channel into "Operator Services" mode +/* set channel into "Operator Services" mode * Option data is a struct oprmode * * \note This option should never be sent over the network */ @@ -433,7 +433,7 @@ struct ast_control_pvt_cause_code { * Option data is a character buffer of suitable length */ #define AST_OPTION_DEVICE_NAME 16 -/*! Get the CC agent type from the channel (Read only) +/*! Get the CC agent type from the channel (Read only) * Option data is a character buffer of suitable length */ #define AST_OPTION_CC_AGENT_TYPE 17 @@ -450,12 +450,12 @@ struct oprmode { struct ast_option_header { /* Always keep in network byte order */ #if __BYTE_ORDER == __BIG_ENDIAN - uint16_t flag:3; - uint16_t option:13; + uint16_t flag:3; + uint16_t option:13; #else #if __BYTE_ORDER == __LITTLE_ENDIAN - uint16_t option:13; - uint16_t flag:3; + uint16_t option:13; + uint16_t flag:3; #else #error Byte order not defined #endif @@ -463,19 +463,19 @@ struct ast_option_header { uint8_t data[0]; }; -/*! \brief Requests a frame to be allocated - * - * \param source - * Request a frame be allocated. source is an optional source of the frame, - * len is the requested length, or "0" if the caller will supply the buffer +/*! \brief Requests a frame to be allocated + * + * \param source + * Request a frame be allocated. source is an optional source of the frame, + * len is the requested length, or "0" if the caller will supply the buffer */ #if 0 /* Unimplemented */ struct ast_frame *ast_fralloc(char *source, int len); #endif -/*! +/*! * \brief Frees a frame or list of frames - * + * * \param fr Frame to free, or head of list to free * \param cache Whether to consider this frame for frame caching */ @@ -483,6 +483,12 @@ void ast_frame_free(struct ast_frame *fr, int cache); #define ast_frfree(fr) ast_frame_free(fr, 1) +/*! + * \brief NULL-safe wrapper for \ref ast_frfree, good for \ref RAII_VAR. + * \param frame Frame to free, or head of list to free. + */ +void ast_frame_dtor(struct ast_frame *frame); + /*! \brief Makes a frame independent of any static storage * \param fr frame to act upon * Take a frame, and if it's not been malloc'd, make a malloc'd copy @@ -498,7 +504,7 @@ void ast_frame_free(struct ast_frame *fr, int cache); */ struct ast_frame *ast_frisolate(struct ast_frame *fr); -/*! \brief Copies a frame +/*! \brief Copies a frame * \param fr frame to copy * Duplicates a frame -- should only rarely be used, typically frisolate is good enough * \return Returns a frame on success, NULL on error @@ -507,7 +513,7 @@ struct ast_frame *ast_frdup(const struct ast_frame *fr); void ast_swapcopy_samples(void *dst, const void *src, int samples); -/* Helpers for byteswapping native samples to/from +/* Helpers for byteswapping native samples to/from little-endian and big-endian. */ #if __BYTE_ORDER == __LITTLE_ENDIAN #define ast_frame_byteswap_le(fr) do { ; } while(0) @@ -518,13 +524,13 @@ void ast_swapcopy_samples(void *dst, const void *src, int samples); #endif /*! \brief Parse an "allow" or "deny" line in a channel or device configuration - and update the capabilities and pref if provided. + and update the capabilities and pref if provided. Video codecs are not added to codec preference lists, since we can not transcode \return Returns number of errors encountered during parsing */ int ast_parse_allow_disallow(struct ast_codec_pref *pref, struct ast_format_cap *cap, const char *list, int allowing); -/*! \name AST_Smoother +/*! \name AST_Smoother */ /*@{ */ /*! \page ast_smooth The AST Frame Smoother @@ -584,7 +590,7 @@ struct ast_frame *ast_frame_enqueue(struct ast_frame *head, struct ast_frame *f, /*! \brief Gets duration in ms of interpolation frame for a format */ static inline int ast_codec_interp_len(struct ast_format *format) -{ +{ return (format->id == AST_FORMAT_ILBC) ? 30 : 20; } diff --git a/include/asterisk/json.h b/include/asterisk/json.h index d8cf98ece1..64a1e7b7a2 100644 --- a/include/asterisk/json.h +++ b/include/asterisk/json.h @@ -764,4 +764,48 @@ struct ast_json *ast_json_deep_copy(const struct ast_json *value); /*!@}*/ +/*!@{*/ + +/*! + * \brief Common JSON rendering functions for common 'objects'. + */ + +/*! + * \brief Simple name/number pair. + * \param name Name + * \param number Number + * \return NULL if error (non-UTF8 characters, NULL inputs, etc.) + * \return JSON object with name and number fields + */ +struct ast_json *ast_json_name_number(const char *name, const char *number); + +/*! + * \brief Construct a timeval as JSON. + * + * JSON does not define a standard date format (boo), but the de facto standard + * is to use ISO 8601 formatted string. We build a millisecond resolution string + * from the \c timeval + * + * \param tv \c timeval to encode. + * \param zone Text string of a standard system zoneinfo file. If NULL, the system localtime will be used. + * \return JSON string with ISO 8601 formatted date/time. + * \return \c NULL on error. + */ +struct ast_json *ast_json_timeval(const struct timeval *tv, const char *zone); + +/*! + * \brief Construct a context/exten/priority as JSON. + * + * If a \c NULL is passed for \c context or \c exten, or -1 for \c priority, + * the fields is set to ast_json_null(). + * + * \param context Context name. + * \param exten Extension. + * \param priority Dialplan priority. + * \return JSON object with \c context, \c exten and \c priority fields + */ +struct ast_json *ast_json_dialplan_cep(const char *context, const char *exten, int priority); + +/*!@}*/ + #endif /* _ASTERISK_JSON_H */ diff --git a/include/asterisk/localtime.h b/include/asterisk/localtime.h index 50f5c835ca..74f342c46a 100644 --- a/include/asterisk/localtime.h +++ b/include/asterisk/localtime.h @@ -99,4 +99,9 @@ char *ast_strptime_locale(const char *s, const char *format, struct ast_tm *tm, struct ast_test; void ast_localtime_wakeup_monitor(struct ast_test *info); +/*! \brief ast_strftime for ISO8601 formatting timestamps. */ +#define AST_ISO8601_FORMAT "%FT%T.%q%z" +/*! \brief Max length of an null terminated, millisecond resolution, ISO8601 timestamp string. */ +#define AST_ISO8601_LEN 29 + #endif /* _ASTERISK_LOCALTIME_H */ diff --git a/main/frame.c b/main/frame.c index a3f67da61b..b559d552d2 100644 --- a/main/frame.c +++ b/main/frame.c @@ -351,6 +351,13 @@ void ast_frame_free(struct ast_frame *frame, int cache) } } +void ast_frame_dtor(struct ast_frame *f) +{ + if (f) { + ast_frfree(f); + } +} + /*! * \brief 'isolates' a frame by duplicating non-malloc'ed components * (header, src, data). diff --git a/main/json.c b/main/json.c index edffcc9451..07d2c5e95b 100644 --- a/main/json.c +++ b/main/json.c @@ -27,6 +27,7 @@ */ /*** MODULEINFO + jansson core ***/ @@ -35,10 +36,12 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/json.h" +#include "asterisk/localtime.h" #include "asterisk/module.h" #include "asterisk/utils.h" #include +#include /*! * \brief Function wrapper around ast_malloc macro. @@ -501,6 +504,37 @@ struct ast_json *ast_json_deep_copy(const struct ast_json *value) return (struct ast_json *)json_deep_copy((json_t *)value); } +struct ast_json *ast_json_name_number(const char *name, const char *number) +{ + return ast_json_pack("{s: s, s: s}", + "name", name, + "number", number); +} + +struct ast_json *ast_json_dialplan_cep(const char *context, const char *exten, int priority) +{ + return ast_json_pack("{s: o, s: o, s: o}", + "context", context ? ast_json_string_create(context) : ast_json_null(), + "exten", exten ? ast_json_string_create(exten) : ast_json_null(), + "priority", priority != -1 ? ast_json_integer_create(priority) : ast_json_null()); +} + +struct ast_json *ast_json_timeval(const struct timeval *tv, const char *zone) +{ + char buf[AST_ISO8601_LEN]; + struct ast_tm tm = {}; + + if (tv == NULL) { + return NULL; + } + + ast_localtime(tv, &tm, zone); + + ast_strftime(buf, sizeof(buf),AST_ISO8601_FORMAT, &tm); + + return ast_json_string_create(buf); +} + void ast_json_init(void) { /* Setup to use Asterisk custom allocators */ diff --git a/res/res_stasis_websocket.c b/res/res_stasis_websocket.c new file mode 100644 index 0000000000..b4819aec99 --- /dev/null +++ b/res/res_stasis_websocket.c @@ -0,0 +1,326 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2012 - 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief HTTP binding for the Stasis API + * + * \author David M. Lee, II + */ + +/*** MODULEINFO + app_stasis + res_http_websocket + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/app_stasis.h" +#include "asterisk/astobj2.h" +#include "asterisk/http_websocket.h" +#include "asterisk/json.h" +#include "asterisk/module.h" +#include "asterisk/strings.h" +#include "asterisk/utils.h" + +/*! WebSocket protocol for Stasis */ +static const char * const ws_protocol = "stasis"; + +/*! Message to send when out of memory */ +static struct ast_json *oom_json; + +/*! Number of buckets for the Stasis application hash table. Remember to keep it + * a prime number! + */ +#define APPS_NUM_BUCKETS 7 + +struct websocket_app { + char *name; +}; + +/*! + * \internal + * \brief Helper to write a JSON object to a WebSocket. + * \param session WebSocket session. + * \param message JSON message. + * \return 0 on success. + * \return -1 on error. + */ +static int websocket_write_json(struct ast_websocket *session, + struct ast_json *message) +{ + RAII_VAR(char *, str, ast_json_dump_string(message), ast_free); + + if (str == NULL) { + ast_log(LOG_ERROR, "Failed to encode JSON object\n"); + return -1; + } + + return ast_websocket_write(session, AST_WEBSOCKET_OPCODE_TEXT, str, + strlen(str)); +} + +/*! Hash function for websocket_app */ +static int hash_app(const void *obj, const int flags) +{ + const struct websocket_app *app = obj; + const char *name = flags & OBJ_KEY ? obj : app->name; + + return ast_str_hash(name); +} + +/*! Comparison function for websocket_app */ +static int compare_app(void *lhs, void *rhs, int flags) +{ + const struct websocket_app *lhs_app = lhs; + const struct websocket_app *rhs_app = rhs; + const char *rhs_name = flags & OBJ_KEY ? rhs : rhs_app->name; + + if (strcmp(lhs_app->name, rhs_name) == 0) { + return CMP_MATCH; + } else { + return 0; + } +} + +static void app_dtor(void *obj) +{ + struct websocket_app *app = obj; + ast_free(app->name); +} + +struct stasis_ws_session_info { + struct ast_websocket *ws_session; + struct ao2_container *websocket_apps; +}; + +static void session_dtor(void *obj) +{ + struct stasis_ws_session_info *session = obj; + + /* session_shutdown should have been called before */ + ast_assert(session->ws_session == NULL); + ast_assert(session->websocket_apps == NULL); +} + +static struct stasis_ws_session_info *session_create( + struct ast_websocket *ws_session) +{ + RAII_VAR(struct stasis_ws_session_info *, session, NULL, ao2_cleanup); + + session = ao2_alloc(sizeof(*session), session_dtor); + + session->ws_session = ws_session; + session->websocket_apps = + ao2_container_alloc(APPS_NUM_BUCKETS, hash_app, compare_app); + + if (!session->websocket_apps) { + return NULL; + } + + ao2_ref(session, +1); + return session; +} + +/*! + * \brief Explicitly shutdown a session. + * + * An explicit shutdown is necessary, since stasis-app has a reference to this + * session. We also need to be sure to null out the \c ws_session field, since + * the websocket is about to go away. + * + * \param session Session info struct. + */ +static void session_shutdown(struct stasis_ws_session_info *session) +{ + struct ao2_iterator i; + struct websocket_app *app; + SCOPED_AO2LOCK(lock, session); + + i = ao2_iterator_init(session->websocket_apps, 0); + while ((app = ao2_iterator_next(&i))) { + stasis_app_unregister(app->name); + ao2_cleanup(app); + } + ao2_iterator_destroy(&i); + ao2_cleanup(session->websocket_apps); + + session->websocket_apps = NULL; + session->ws_session = NULL; +} + +/*! + * \brief Callback handler for Stasis application messages. + */ +static void app_handler(void *data, const char *app_name, + struct ast_json *message) +{ + struct stasis_ws_session_info *session = data; + int res; + + res = ast_json_object_set(message, "application", + ast_json_string_create(app_name)); + if(res != 0) { + return; + } + + ao2_lock(session); + if (session->ws_session) { + websocket_write_json(session->ws_session, message); + } + ao2_unlock(session); +} + +/*! + * \brief Register for all of the apps given. + * \param session Session info struct. + * \param app_list Comma seperated list of app names to register. + */ +static int session_register_apps(struct stasis_ws_session_info *session, + const char *app_list) +{ + RAII_VAR(char *, to_free, NULL, ast_free); + char *apps, *app_name; + SCOPED_AO2LOCK(lock, session); + + ast_assert(session->ws_session != NULL); + ast_assert(session->websocket_apps != NULL); + + to_free = apps = ast_strdup(app_list); + if (!apps) { + websocket_write_json(session->ws_session, oom_json); + return -1; + } + while ((app_name = strsep(&apps, ","))) { + RAII_VAR(struct websocket_app *, app, NULL, ao2_cleanup); + + app = ao2_alloc(sizeof(*app), app_dtor); + if (!app) { + websocket_write_json(session->ws_session, oom_json); + return -1; + } + app->name = ast_strdup(app_name); + ao2_link(session->websocket_apps, app); + + stasis_app_register(app_name, app_handler, session); + } + return 0; +} + +static void websocket_callback(struct ast_websocket *ws_session, + struct ast_variable *parameters, + struct ast_variable *headers) +{ + RAII_VAR(struct stasis_ws_session_info *, stasis_session, NULL, ao2_cleanup); + struct ast_variable *param = NULL; + int res; + + ast_debug(3, "Stasis web socket connection\n"); + + if (ast_websocket_set_nonblock(ws_session) != 0) { + ast_log(LOG_ERROR, + "Stasis web socket failed to set nonblock; closing\n"); + goto end; + } + + stasis_session = session_create(ws_session); + + if (!stasis_session) { + websocket_write_json(ws_session, oom_json); + goto end; + } + + for (param = parameters; param; param = param->next) { + if (strcmp(param->name, "app") == 0) { + int ret = session_register_apps( + stasis_session, param->value); + if (ret != 0) { + goto end; + } + } + } + + if (ao2_container_count(stasis_session->websocket_apps) == 0) { + RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref); + + msg = ast_json_pack("{s: s, s: [s]}", + "error", "MissingParams", + "params", "app"); + if (msg) { + websocket_write_json(ws_session, msg); + } + + goto end; + } + + while ((res = ast_wait_for_input(ast_websocket_fd(ws_session), -1)) > 0) { + char *payload; + uint64_t payload_len; + enum ast_websocket_opcode opcode; + int fragmented; + int read = ast_websocket_read(ws_session, &payload, &payload_len, + &opcode, &fragmented); + + if (read) { + ast_log(LOG_ERROR, + "Stasis WebSocket read error; closing\n"); + break; + } + + if (opcode == AST_WEBSOCKET_OPCODE_CLOSE) { + break; + } + } + +end: + session_shutdown(stasis_session); + ast_websocket_unref(ws_session); +} + +static int load_module(void) +{ + int r = 0; + + oom_json = ast_json_pack("{s: s}", + "error", "OutOfMemory"); + if (!oom_json) { + /* ironic */ + return AST_MODULE_LOAD_FAILURE; + } + r |= ast_websocket_add_protocol(ws_protocol, websocket_callback); + return r; +} + +static int unload_module(void) +{ + int r = 0; + + ast_json_unref(oom_json); + oom_json = NULL; + r |= ast_websocket_remove_protocol(ws_protocol, websocket_callback); + return r; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, 0, "Stasis HTTP bindings", + .load = load_module, + .unload = unload_module, + .nonoptreq = "app_stasis,res_http_websocket" + ); diff --git a/tests/test_abstract_jb.c b/tests/test_abstract_jb.c index f39b0b03b6..17861e6bc5 100644 --- a/tests/test_abstract_jb.c +++ b/tests/test_abstract_jb.c @@ -67,17 +67,6 @@ static void dispose_jitterbuffer(struct ast_jb *jb) jb->jbobj = NULL; } -/*! \internal \brief Destructor for frames - * \param f The frame to destroy - */ -static void dispose_frame(struct ast_frame *f) -{ - if (!f) { - return; - } - ast_frfree(f); -} - /*! \internal \brief Create a test frame * \param timestamp the time in ms of the frame * \param seqno the frame's sequence number @@ -217,8 +206,8 @@ static struct ast_jb default_jb = { RAII_VAR(struct ast_jb *, jb, &default_jb, dispose_jitterbuffer); \ const struct ast_jb_impl *impl; \ struct ast_jb_conf conf; \ - RAII_VAR(struct ast_frame *, expected_frame, NULL, dispose_frame); \ - RAII_VAR(struct ast_frame *, actual_frame, NULL, dispose_frame); \ + RAII_VAR(struct ast_frame *, expected_frame, NULL, ast_frame_dtor); \ + RAII_VAR(struct ast_frame *, actual_frame, NULL, ast_frame_dtor); \ int res; \ \ switch (cmd) { \ @@ -273,8 +262,8 @@ static struct ast_jb default_jb = { RAII_VAR(struct ast_jb *, jb, &default_jb, dispose_jitterbuffer); \ const struct ast_jb_impl *impl; \ struct ast_jb_conf conf; \ - RAII_VAR(struct ast_frame *, expected_frame, NULL, dispose_frame); \ - RAII_VAR(struct ast_frame *, actual_frame, NULL, dispose_frame); \ + RAII_VAR(struct ast_frame *, expected_frame, NULL, ast_frame_dtor); \ + RAII_VAR(struct ast_frame *, actual_frame, NULL, ast_frame_dtor); \ int res; \ long next; \ int i; \ @@ -336,7 +325,7 @@ static struct ast_jb default_jb = { RAII_VAR(struct ast_jb *, jb, &default_jb, dispose_jitterbuffer); \ const struct ast_jb_impl *impl; \ struct ast_jb_conf conf; \ - RAII_VAR(struct ast_frame *, expected_frame, NULL, dispose_frame); \ + RAII_VAR(struct ast_frame *, expected_frame, NULL, ast_frame_dtor); \ int res; \ int i; \ \ @@ -401,8 +390,8 @@ static struct ast_jb default_jb = { RAII_VAR(struct ast_jb *, jb, &default_jb, dispose_jitterbuffer); \ const struct ast_jb_impl *impl; \ struct ast_jb_conf conf; \ - RAII_VAR(struct ast_frame *, actual_frame, NULL, dispose_frame); \ - RAII_VAR(struct ast_frame *, expected_frame, NULL, dispose_frame); \ + RAII_VAR(struct ast_frame *, actual_frame, NULL, ast_frame_dtor); \ + RAII_VAR(struct ast_frame *, expected_frame, NULL, ast_frame_dtor); \ int res; \ long next; \ int i; \ diff --git a/tests/test_app_stasis.c b/tests/test_app_stasis.c new file mode 100644 index 0000000000..02eb85af0b --- /dev/null +++ b/tests/test_app_stasis.c @@ -0,0 +1,194 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * David M. Lee, II + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! + * \file \brief Test Stasis Application API. + * \author\verbatim David M. Lee, II \endverbatim + * + * \ingroup tests + */ + +/*** MODULEINFO + TEST_FRAMEWORK + app_stasis + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/module.h" +#include "asterisk/test.h" +#include "asterisk/app_stasis.h" + +static const char *test_category = "/stasis/app/"; + +AST_TEST_DEFINE(app_invoke_dne) +{ + int res; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = test_category; + info->summary = "Test stasis app invocation."; + info->description = "Test stasis app invocation."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + res = stasis_app_send("i-am-not-an-app", ast_json_null()); + ast_test_validate(test, -1 == res); + + return AST_TEST_PASS; +} + +struct app_data { + int invocations; + struct ast_json *messages; +}; + +static void app_data_dtor(void *obj) +{ + struct app_data *actual = obj; + + ast_json_unref(actual->messages); + actual->messages = NULL; +} + +static struct app_data *app_data_create(void) +{ + struct app_data *res = ao2_alloc(sizeof(struct app_data), app_data_dtor); + + if (!res) { + return NULL; + } + + res->messages = ast_json_array_create(); + return res; +} + +static void test_handler(void *data, const char *app_name, struct ast_json *message) +{ + struct app_data *actual = data; + int res; + ++(actual->invocations); + res = ast_json_array_append(actual->messages, ast_json_copy(message)); + ast_assert(res == 0); +} + +AST_TEST_DEFINE(app_invoke_one) +{ + RAII_VAR(struct app_data *, app_data, NULL, ao2_cleanup); + RAII_VAR(char *, app_name, NULL, stasis_app_unregister); + RAII_VAR(struct ast_json *, expected_message, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); + int res; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = test_category; + info->summary = "Test stasis app invocation."; + info->description = "Test stasis app invocation."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + app_name = "test-handler"; + + app_data = app_data_create(); + + stasis_app_register(app_name, test_handler, app_data); + message = ast_json_pack("{ s: o }", "test-message", ast_json_null()); + expected_message = ast_json_pack("[o]", ast_json_ref(message)); + + res = stasis_app_send(app_name, message); + ast_test_validate(test, 0 == res); + ast_test_validate(test, 1 == app_data->invocations); + ast_test_validate(test, ast_json_equal(expected_message, app_data->messages)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(app_replaced) +{ + RAII_VAR(struct app_data *, app_data1, NULL, ao2_cleanup); + RAII_VAR(struct app_data *, app_data2, NULL, ao2_cleanup); + RAII_VAR(char *, app_name, NULL, stasis_app_unregister); + RAII_VAR(struct ast_json *, expected_message1, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, expected_message2, NULL, ast_json_unref); + int res; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = test_category; + info->summary = "Test stasis app invocation."; + info->description = "Test stasis app invocation."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + app_name = "test-handler"; + + app_data1 = app_data_create(); + app_data2 = app_data_create(); + + stasis_app_register(app_name, test_handler, app_data1); + stasis_app_register(app_name, test_handler, app_data2); + expected_message1 = ast_json_pack("[{s: {}}]", "application-replaced"); + message = ast_json_pack("{ s: o }", "test-message", ast_json_null()); + expected_message2 = ast_json_pack("[o]", ast_json_ref(message)); + + res = stasis_app_send(app_name, message); + ast_test_validate(test, 0 == res); + ast_test_validate(test, 1 == app_data1->invocations); + ast_test_validate(test, ast_json_equal(expected_message1, app_data1->messages)); + ast_test_validate(test, 1 == app_data2->invocations); + ast_test_validate(test, ast_json_equal(expected_message2, app_data2->messages)); + + return AST_TEST_PASS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(app_invoke_dne); + AST_TEST_UNREGISTER(app_invoke_one); + AST_TEST_UNREGISTER(app_replaced); + return 0; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(app_replaced); + AST_TEST_REGISTER(app_invoke_one); + AST_TEST_REGISTER(app_invoke_dne); + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, 0, "Stasis Core testing", + .load = load_module, + .unload = unload_module, + .nonoptreq = "app_stasis" + ); diff --git a/tests/test_json.c b/tests/test_json.c index 6038672790..9b4be5bebf 100644 --- a/tests/test_json.c +++ b/tests/test_json.c @@ -1611,6 +1611,102 @@ AST_TEST_DEFINE(json_test_clever_circle) return AST_TEST_PASS; } +AST_TEST_DEFINE(json_test_name_number) +{ + RAII_VAR(void *, alloc_debug, json_test_init(test), json_test_finish); + RAII_VAR(struct ast_json *, uut, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref); + + switch (cmd) { + case TEST_INIT: + info->name = "name_number"; + info->category = "/main/json/"; + info->summary = "JSON encoding of name/number pair."; + info->description = "Test JSON abstraction library."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, NULL == ast_json_name_number("name", NULL)); + ast_test_validate(test, NULL == ast_json_name_number(NULL, "1234")); + ast_test_validate(test, NULL == ast_json_name_number(NULL, NULL)); + + expected = ast_json_pack("{s: s, s: s}", + "name", "Jenny", + "number", "867-5309"); + uut = ast_json_name_number("Jenny", "867-5309"); + ast_test_validate(test, ast_json_equal(expected, uut)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(json_test_timeval) +{ + RAII_VAR(void *, alloc_debug, json_test_init(test), json_test_finish); + RAII_VAR(struct ast_json *, uut, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref); + struct timeval tv = {}; + + switch (cmd) { + case TEST_INIT: + info->name = "timeval"; + info->category = "/main/json/"; + info->summary = "JSON encoding of timevals."; + info->description = "Test JSON abstraction library."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, NULL == ast_json_timeval(NULL, NULL)); + expected = ast_json_string_create("2013-02-07T09:32:34.314-0600"); + + tv.tv_sec = 1360251154; + tv.tv_usec = 314159; + uut = ast_json_timeval(&tv, "America/Chicago"); + + ast_test_validate(test, ast_json_equal(expected, uut)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(json_test_cep) +{ + RAII_VAR(void *, alloc_debug, json_test_init(test), json_test_finish); + RAII_VAR(struct ast_json *, uut, NULL, ast_json_unref); + RAII_VAR(struct ast_json *, expected, NULL, ast_json_unref); + + switch (cmd) { + case TEST_INIT: + info->name = "cep"; + info->category = "/main/json/"; + info->summary = "JSON with circular references cannot be encoded."; + info->description = "Test JSON abstraction library."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + expected = ast_json_pack("{s: o, s: o, s: o}", + "context", ast_json_null(), + "exten", ast_json_null(), + "priority", ast_json_null()); + uut = ast_json_dialplan_cep(NULL, NULL, -1); + ast_test_validate(test, ast_json_equal(expected, uut)); + + ast_json_unref(expected); + ast_json_unref(uut); + expected = ast_json_pack("{s: s, s: s, s: i}", + "context", "main", + "exten", "4321", + "priority", 7); + uut = ast_json_dialplan_cep("main", "4321", 7); + ast_test_validate(test, ast_json_equal(expected, uut)); + + return AST_TEST_PASS; +} + static int unload_module(void) { AST_TEST_UNREGISTER(json_test_false); @@ -1661,6 +1757,9 @@ static int unload_module(void) AST_TEST_UNREGISTER(json_test_circular_object); AST_TEST_UNREGISTER(json_test_circular_array); AST_TEST_UNREGISTER(json_test_clever_circle); + AST_TEST_UNREGISTER(json_test_name_number); + AST_TEST_UNREGISTER(json_test_timeval); + AST_TEST_UNREGISTER(json_test_cep); return 0; } @@ -1714,6 +1813,9 @@ static int load_module(void) AST_TEST_REGISTER(json_test_circular_object); AST_TEST_REGISTER(json_test_circular_array); AST_TEST_REGISTER(json_test_clever_circle); + AST_TEST_REGISTER(json_test_name_number); + AST_TEST_REGISTER(json_test_timeval); + AST_TEST_REGISTER(json_test_cep); return AST_MODULE_LOAD_SUCCESS; }