]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
res_stasis: Add ability to switch applications.
authorBen Ford <bford@digium.com>
Thu, 7 Mar 2019 13:52:20 +0000 (07:52 -0600)
committerBen Ford <bford@digium.com>
Thu, 7 Mar 2019 13:53:01 +0000 (07:53 -0600)
Added the ability to move between Stasis applications within Stasis.
This can be done by calling 'move' in an application, providing (at
minimum) the channel's id and the application to switch to. If the
application is not registered or active, nothing will happen and the
channel will remain in the current application, and an event will be
triggered to let the application know that the move failed. The event
name is "ApplicationMoveFailed", and provides the "destination" that the
channel was attempting to move to, as well as the usual channel
information. Optionally, a list of arguments can be passed to the
function call for the receiving application. A full example of a 'move'
call would look like this:

client.channels.move(channelId, app, appArgs)

The control object used to control the channel in Stasis can now switch
which application it belongs to, rather than belonging to one Stasis
application for its lifetime. This allows us to use the same control
object instead of having to tear down the current one and create
another.

ASTERISK-28267 #close

Change-Id: I43d12b10045a98a8d42541889b85695be26f288a

12 files changed:
CHANGES
include/asterisk/stasis_app.h
res/ari/ari_model_validators.c
res/ari/ari_model_validators.h
res/ari/resource_channels.c
res/ari/resource_channels.h
res/res_ari_channels.c
res/res_stasis.c
res/stasis/control.c
res/stasis/control.h
rest-api/api-docs/channels.json
rest-api/api-docs/events.json

diff --git a/CHANGES b/CHANGES
index d347507435d03c78b4aacf79fa7692d8d8182f2e..60d8a4dea1fd33da97d042dda638a9f319800d2a 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -83,6 +83,20 @@ ARI
    types defined in the "disallowed" list are not sent to the application. Note
    that if a type is specified in both lists "disallowed" takes precedence.
 
+ * A new REST API call has been added: 'move'. It follows the format
+   'channels/{channelId}/move' and can be used to move channels from one application
+   to another without needing to exit back into the dialplan. An application must be
+   specified, but the passing a list of arguments to the new application is optional.
+   An example call would look like this:
+
+   client.channels.move(channelId=chan.id, app='ari-example', appArgs='a,b,c')
+
+   If the channel was inside of a bridge when switching applications, it will
+   remain there. If the application specified cannot be moved to, then the channel
+   will remain in the current application and an event will be triggered named
+   "ApplicationMoveFailed", which will provide the destination application's name
+   and the channel information.
+
 res_pjsip
 ------------------
  * A new configuration parameter "taskprocessor_overload_trigger" has been
index c40e901e5f39dcc7694b9c138d179e5182c7b60a..01c7ff4d7cbd9bff31504cc794de9dbd9473dff6 100644 (file)
@@ -501,6 +501,20 @@ void stasis_app_control_clear_roles(struct stasis_app_control *control);
  */
 int stasis_app_control_continue(struct stasis_app_control *control, const char *context, const char *extension, int priority);
 
+/*!
+ * \brief Exit \c res_stasis and move to another Stasis application.
+ *
+ * If the channel is no longer in \c res_stasis, this function does nothing.
+ *
+ * \param control Control for \c res_stasis
+ * \param app_name The name of the application to switch to
+ * \param app_args The list of arguments to pass to the application
+ *
+ * \return 0 for success
+ * \return -1 for error
+ */
+int stasis_app_control_move(struct stasis_app_control *control, const char *app_name, const char *app_args);
+
 /*!
  * \brief Redirect a channel in \c res_stasis to a particular endpoint
  *
index 44d9d7727cc7daf4ff0d0c0c37a82adc4474999e..254de3c271bf028cc42ba5c5401cd806b0fdb05f 100644 (file)
@@ -2028,6 +2028,127 @@ ari_validator ast_ari_validate_mailbox_fn(void)
        return ast_ari_validate_mailbox;
 }
 
+int ast_ari_validate_application_move_failed(struct ast_json *json)
+{
+       int res = 1;
+       struct ast_json_iter *iter;
+       int has_type = 0;
+       int has_application = 0;
+       int has_args = 0;
+       int has_channel = 0;
+       int has_destination = 0;
+
+       for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) {
+               if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       prop_is_valid = ast_ari_validate_string(
+                               ast_json_object_iter_value(iter));
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field asterisk_id failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("type", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_type = 1;
+                       prop_is_valid = ast_ari_validate_string(
+                               ast_json_object_iter_value(iter));
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field type failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("application", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_application = 1;
+                       prop_is_valid = ast_ari_validate_string(
+                               ast_json_object_iter_value(iter));
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field application failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       prop_is_valid = ast_ari_validate_date(
+                               ast_json_object_iter_value(iter));
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field timestamp failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("args", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_args = 1;
+                       prop_is_valid = ast_ari_validate_list(
+                               ast_json_object_iter_value(iter),
+                               ast_ari_validate_string);
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field args failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("channel", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_channel = 1;
+                       prop_is_valid = ast_ari_validate_channel(
+                               ast_json_object_iter_value(iter));
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field channel failed validation\n");
+                               res = 0;
+                       }
+               } else
+               if (strcmp("destination", ast_json_object_iter_key(iter)) == 0) {
+                       int prop_is_valid;
+                       has_destination = 1;
+                       prop_is_valid = ast_ari_validate_string(
+                               ast_json_object_iter_value(iter));
+                       if (!prop_is_valid) {
+                               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field destination failed validation\n");
+                               res = 0;
+                       }
+               } else
+               {
+                       ast_log(LOG_ERROR,
+                               "ARI ApplicationMoveFailed has undocumented field %s\n",
+                               ast_json_object_iter_key(iter));
+                       res = 0;
+               }
+       }
+
+       if (!has_type) {
+               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field type\n");
+               res = 0;
+       }
+
+       if (!has_application) {
+               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field application\n");
+               res = 0;
+       }
+
+       if (!has_args) {
+               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field args\n");
+               res = 0;
+       }
+
+       if (!has_channel) {
+               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field channel\n");
+               res = 0;
+       }
+
+       if (!has_destination) {
+               ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field destination\n");
+               res = 0;
+       }
+
+       return res;
+}
+
+ari_validator ast_ari_validate_application_move_failed_fn(void)
+{
+       return ast_ari_validate_application_move_failed;
+}
+
 int ast_ari_validate_application_replaced(struct ast_json *json)
 {
        int res = 1;
@@ -5095,6 +5216,9 @@ int ast_ari_validate_event(struct ast_json *json)
        if (strcmp("Event", discriminator) == 0) {
                /* Self type; fall through */
        } else
+       if (strcmp("ApplicationMoveFailed", discriminator) == 0) {
+               return ast_ari_validate_application_move_failed(json);
+       } else
        if (strcmp("ApplicationReplaced", discriminator) == 0) {
                return ast_ari_validate_application_replaced(json);
        } else
@@ -5293,6 +5417,9 @@ int ast_ari_validate_message(struct ast_json *json)
        if (strcmp("Message", discriminator) == 0) {
                /* Self type; fall through */
        } else
+       if (strcmp("ApplicationMoveFailed", discriminator) == 0) {
+               return ast_ari_validate_application_move_failed(json);
+       } else
        if (strcmp("ApplicationReplaced", discriminator) == 0) {
                return ast_ari_validate_application_replaced(json);
        } else
index 1ee74f4865b83eaaf54b1a74b333dfb40f2040f5..4b3269af6a73d3fd9f3492e45c3e7dde97562f2f 100644 (file)
@@ -623,6 +623,24 @@ int ast_ari_validate_mailbox(struct ast_json *json);
  */
 ari_validator ast_ari_validate_mailbox_fn(void);
 
+/*!
+ * \brief Validator for ApplicationMoveFailed.
+ *
+ * Notification that trying to move a channel to another Stasis application failed.
+ *
+ * \param json JSON object to validate.
+ * \returns True (non-zero) if valid.
+ * \returns False (zero) if invalid.
+ */
+int ast_ari_validate_application_move_failed(struct ast_json *json);
+
+/*!
+ * \brief Function pointer to ast_ari_validate_application_move_failed().
+ *
+ * See \ref ast_ari_model_validators.h for more details.
+ */
+ari_validator ast_ari_validate_application_move_failed_fn(void);
+
 /*!
  * \brief Validator for ApplicationReplaced.
  *
@@ -1527,6 +1545,14 @@ ari_validator ast_ari_validate_application_fn(void);
  * - name: string (required)
  * - new_messages: int (required)
  * - old_messages: int (required)
+ * ApplicationMoveFailed
+ * - asterisk_id: string
+ * - type: string (required)
+ * - application: string (required)
+ * - timestamp: Date
+ * - args: List[string] (required)
+ * - channel: Channel (required)
+ * - destination: string (required)
  * ApplicationReplaced
  * - asterisk_id: string
  * - type: string (required)
index 08f97f1d34b68e3f5810bc555f2a3c5c144c092c..eca70cec8a7a972a7da44b0fe6f7262c805dc938 100644 (file)
@@ -217,6 +217,26 @@ void ast_ari_channels_continue_in_dialplan(
        ast_ari_response_no_content(response);
 }
 
+void ast_ari_channels_move(struct ast_variable *headers,
+       struct ast_ari_channels_move_args *args,
+       struct ast_ari_response *response)
+{
+       RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup);
+
+       control = find_control(response, args->channel_id);
+       if (!control) {
+               return;
+       }
+
+       if (stasis_app_control_move(control, args->app, args->app_args)) {
+               ast_ari_response_error(response, 500, "Internal Server Error",
+                       "Failed to switch Stasis applications");
+               return;
+       }
+
+       ast_ari_response_no_content(response);
+}
+
 void ast_ari_channels_redirect(struct ast_variable *headers,
        struct ast_ari_channels_redirect_args *args,
        struct ast_ari_response *response)
index b071d08baf237a59bcbbc7b2994e16783770cdeb..fdd7a6b7812d775bab15afa202a180567a35561a 100644 (file)
@@ -261,6 +261,34 @@ int ast_ari_channels_continue_in_dialplan_parse_body(
  * \param[out] response HTTP response
  */
 void ast_ari_channels_continue_in_dialplan(struct ast_variable *headers, struct ast_ari_channels_continue_in_dialplan_args *args, struct ast_ari_response *response);
+/*! Argument struct for ast_ari_channels_move() */
+struct ast_ari_channels_move_args {
+       /*! Channel's id */
+       const char *channel_id;
+       /*! The channel will be passed to this Stasis application. */
+       const char *app;
+       /*! The application arguments to pass to the Stasis application provided by 'app'. */
+       const char *app_args;
+};
+/*!
+ * \brief Body parsing function for /channels/{channelId}/move.
+ * \param body The JSON body from which to parse parameters.
+ * \param[out] args The args structure to parse into.
+ * \retval zero on success
+ * \retval non-zero on failure
+ */
+int ast_ari_channels_move_parse_body(
+       struct ast_json *body,
+       struct ast_ari_channels_move_args *args);
+
+/*!
+ * \brief Move the channel from one Stasis application to another.
+ *
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ * \param[out] response HTTP response
+ */
+void ast_ari_channels_move(struct ast_variable *headers, struct ast_ari_channels_move_args *args, struct ast_ari_response *response);
 /*! Argument struct for ast_ari_channels_redirect() */
 struct ast_ari_channels_redirect_args {
        /*! Channel's id */
index dae146c43c0a93a1345d04f635960596fb6ea62e..3d96d60e4cd8d74f17fc3460222bd1f763f7481b 100644 (file)
@@ -775,6 +775,95 @@ static void ast_ari_channels_continue_in_dialplan_cb(
        }
 #endif /* AST_DEVMODE */
 
+fin: __attribute__((unused))
+       return;
+}
+int ast_ari_channels_move_parse_body(
+       struct ast_json *body,
+       struct ast_ari_channels_move_args *args)
+{
+       struct ast_json *field;
+       /* Parse query parameters out of it */
+       field = ast_json_object_get(body, "app");
+       if (field) {
+               args->app = ast_json_string_get(field);
+       }
+       field = ast_json_object_get(body, "appArgs");
+       if (field) {
+               args->app_args = ast_json_string_get(field);
+       }
+       return 0;
+}
+
+/*!
+ * \brief Parameter parsing callback for /channels/{channelId}/move.
+ * \param get_params GET parameters in the HTTP request.
+ * \param path_vars Path variables extracted from the request.
+ * \param headers HTTP headers.
+ * \param[out] response Response to the HTTP request.
+ */
+static void ast_ari_channels_move_cb(
+       struct ast_tcptls_session_instance *ser,
+       struct ast_variable *get_params, struct ast_variable *path_vars,
+       struct ast_variable *headers, struct ast_json *body, struct ast_ari_response *response)
+{
+       struct ast_ari_channels_move_args args = {};
+       struct ast_variable *i;
+#if defined(AST_DEVMODE)
+       int is_valid;
+       int code;
+#endif /* AST_DEVMODE */
+
+       for (i = get_params; i; i = i->next) {
+               if (strcmp(i->name, "app") == 0) {
+                       args.app = (i->value);
+               } else
+               if (strcmp(i->name, "appArgs") == 0) {
+                       args.app_args = (i->value);
+               } else
+               {}
+       }
+       for (i = path_vars; i; i = i->next) {
+               if (strcmp(i->name, "channelId") == 0) {
+                       args.channel_id = (i->value);
+               } else
+               {}
+       }
+       if (ast_ari_channels_move_parse_body(body, &args)) {
+               ast_ari_response_alloc_failed(response);
+               goto fin;
+       }
+       ast_ari_channels_move(headers, &args, response);
+#if defined(AST_DEVMODE)
+       code = response->response_code;
+
+       switch (code) {
+       case 0: /* Implementation is still a stub, or the code wasn't set */
+               is_valid = response->message == NULL;
+               break;
+       case 500: /* Internal Server Error */
+       case 501: /* Not Implemented */
+       case 404: /* Channel not found */
+       case 409: /* Channel not in a Stasis application */
+               is_valid = 1;
+               break;
+       default:
+               if (200 <= code && code <= 299) {
+                       is_valid = ast_ari_validate_void(
+                               response->message);
+               } else {
+                       ast_log(LOG_ERROR, "Invalid error response %d for /channels/{channelId}/move\n", code);
+                       is_valid = 0;
+               }
+       }
+
+       if (!is_valid) {
+               ast_log(LOG_ERROR, "Response validation failed for /channels/{channelId}/move\n");
+               ast_ari_response_error(response, 500,
+                       "Internal Server Error", "Response validation failed");
+       }
+#endif /* AST_DEVMODE */
+
 fin: __attribute__((unused))
        return;
 }
@@ -2680,6 +2769,15 @@ static struct stasis_rest_handlers channels_channelId_continue = {
        .children = {  }
 };
 /*! \brief REST handler for /api-docs/channels.json */
+static struct stasis_rest_handlers channels_channelId_move = {
+       .path_segment = "move",
+       .callbacks = {
+               [AST_HTTP_POST] = ast_ari_channels_move_cb,
+       },
+       .num_children = 0,
+       .children = {  }
+};
+/*! \brief REST handler for /api-docs/channels.json */
 static struct stasis_rest_handlers channels_channelId_redirect = {
        .path_segment = "redirect",
        .callbacks = {
@@ -2831,8 +2929,8 @@ static struct stasis_rest_handlers channels_channelId = {
                [AST_HTTP_POST] = ast_ari_channels_originate_with_id_cb,
                [AST_HTTP_DELETE] = ast_ari_channels_hangup_cb,
        },
-       .num_children = 14,
-       .children = { &channels_channelId_continue,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial, }
+       .num_children = 15,
+       .children = { &channels_channelId_continue,&channels_channelId_move,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial, }
 };
 /*! \brief REST handler for /api-docs/channels.json */
 static struct stasis_rest_handlers channels = {
index 4da78a6dc0fe25c19b6e6d243d28126189c27db4..4b7c3ed74cf2f643c18dc9528adaf68e3ee3e17b 100644 (file)
@@ -1324,7 +1324,17 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 
        control = control_create(chan, app);
        if (!control) {
-               ast_log(LOG_ERROR, "Allocated failed\n");
+               ast_log(LOG_ERROR, "Control allocation failed or Stasis app '%s' not registered\n", app_name);
+               return -1;
+       }
+
+       if (!control_app(control)) {
+               ast_log(LOG_ERROR, "Stasis app '%s' not registered\n", app_name);
+               return -1;
+       }
+
+       if (!app_is_active(control_app(control))) {
+               ast_log(LOG_ERROR, "Stasis app '%s' not active\n", app_name);
                return -1;
        }
        ao2_link(app_controls, control);
@@ -1334,7 +1344,7 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
                return -1;
        }
 
-       res = send_start_msg(app, chan, argc, argv);
+       res = send_start_msg(control_app(control), chan, argc, argv);
        if (res != 0) {
                ast_log(LOG_ERROR,
                        "Error sending start message to '%s'\n", app_name);
@@ -1357,15 +1367,138 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
                        break;
                }
 
+               /* control->next_app is only modified within the control thread, so this is safe */
+               if (control_next_app(control)) {
+                       struct stasis_app *next_app = ao2_find(apps_registry, control_next_app(control), OBJ_SEARCH_KEY);
+
+                       if (next_app && app_is_active(next_app)) {
+                               int idx;
+                               int next_argc;
+                               char **next_argv;
+
+                               /* If something goes wrong in this conditional, res will need to be non-zero
+                                * so that the code below the exec loop knows something went wrong during a move.
+                                */
+                               if (!stasis_app_channel_is_stasis_end_published(chan)) {
+                                       res = has_masquerade_store(chan) && app_send_end_msg(control_app(control), chan);
+                                       if (res != 0) {
+                                               ast_log(LOG_ERROR,
+                                                       "Error sending end message to %s\n", stasis_app_name(control_app(control)));
+                                               control_mark_done(control);
+                                               ao2_ref(next_app, -1);
+                                               break;
+                                       }
+                               } else {
+                                       remove_stasis_end_published(chan);
+                               }
+
+                               /* This will ao2_bump next_app, and unref the previous app by 1 */
+                               control_set_app(control, next_app);
+
+                               /* There's a chance that the previous application is ready for clean up, so go ahead
+                                * and do that now.
+                                */
+                               cleanup();
+
+                               /* We need to add another masquerade store, otherwise the leave message will
+                                * not show up for the correct application.
+                                */
+                               if (add_masquerade_store(chan)) {
+                                       ast_log(LOG_ERROR, "Failed to attach masquerade detector\n");
+                                       res = -1;
+                                       control_mark_done(control);
+                                       ao2_ref(next_app, -1);
+                                       break;
+                               }
+
+                               /* We MUST get the size before the list, as control_next_app_args steals the elements
+                                * from the string vector.
+                                */
+                               next_argc = control_next_app_args_size(control);
+                               next_argv = control_next_app_args(control);
+
+                               res = send_start_msg(control_app(control), chan, next_argc, next_argv);
+
+                               /* Even if res != 0, we still need to free the memory we got from control_argv */
+                               if (next_argv) {
+                                       for (idx = 0; idx < next_argc; idx++) {
+                                               ast_free(next_argv[idx]);
+                                       }
+                                       ast_free(next_argv);
+                               }
+
+                               if (res != 0) {
+                                       ast_log(LOG_ERROR,
+                                               "Error sending start message to '%s'\n", stasis_app_name(control_app(control)));
+                                       remove_masquerade_store(chan);
+                                       control_mark_done(control);
+                                       ao2_ref(next_app, -1);
+                                       break;
+                               }
+
+                               /* Done switching applications, free memory and clean up */
+                               control_move_cleanup(control);
+                       } else {
+                               /* If we can't switch applications, do nothing */
+                               struct ast_json *msg;
+                               RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup);
+
+                               if (!next_app) {
+                                       ast_log(LOG_ERROR, "Could not move to Stasis app '%s' - not registered\n",
+                                               control_next_app(control));
+                               } else {
+                                       ast_log(LOG_ERROR, "Could not move to Stasis app '%s' - not active\n",
+                                               control_next_app(control));
+                               }
+
+                               snapshot = ast_channel_snapshot_get_latest(ast_channel_uniqueid(chan));
+                               if (!snapshot) {
+                                       ast_log(LOG_ERROR, "Could not get channel shapshot for '%s'\n",
+                                               ast_channel_name(chan));
+                               } else {
+                                       struct ast_json *json_args;
+                                       int next_argc = control_next_app_args_size(control);
+                                       char **next_argv = control_next_app_args(control);
+
+                                       msg = ast_json_pack("{s: s, s: o, s: s, s: []}",
+                                               "type", "ApplicationMoveFailed",
+                                               "channel", ast_channel_snapshot_to_json(snapshot, NULL),
+                                               "destination", control_next_app(control),
+                                               "args");
+                                       json_args = ast_json_object_get(msg, "args");
+                                       if (!json_args) {
+                                               ast_log(LOG_ERROR, "Could not get args json array");
+                                       } else {
+                                               int r = 0;
+                                               int idx;
+                                               for (idx = 0; idx < next_argc; ++idx) {
+                                                       r = ast_json_array_append(json_args,
+                                                               ast_json_string_create(next_argv[idx]));
+                                                       if (r != 0) {
+                                                               ast_log(LOG_ERROR, "Error appending to ApplicationMoveFailed message\n");
+                                                               break;
+                                                       }
+                                               }
+                                               if (r == 0) {
+                                                       app_send(control_app(control), msg);
+                                               }
+                                       }
+                                       ast_json_unref(msg);
+                               }
+                       }
+                       control_move_cleanup(control);
+                       ao2_cleanup(next_app);
+               }
+
                last_bridge = bridge;
                bridge = ao2_bump(stasis_app_get_bridge(control));
 
                if (bridge != last_bridge) {
                        if (last_bridge) {
-                               app_unsubscribe_bridge(app, last_bridge);
+                               app_unsubscribe_bridge(control_app(control), last_bridge);
                        }
                        if (bridge) {
-                               app_subscribe_bridge(app, bridge);
+                               app_subscribe_bridge(control_app(control), bridge);
                        }
                }
 
@@ -1425,18 +1558,18 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
        }
 
        if (stasis_app_get_bridge(control)) {
-               app_unsubscribe_bridge(app, stasis_app_get_bridge(control));
+               app_unsubscribe_bridge(control_app(control), stasis_app_get_bridge(control));
        }
        ao2_cleanup(bridge);
 
        /* Only publish a stasis_end event if it hasn't already been published */
-       if (!stasis_app_channel_is_stasis_end_published(chan)) {
+       if (!res && !stasis_app_channel_is_stasis_end_published(chan)) {
                /* A masquerade has occurred and this message will be wrong so it
                 * has already been sent elsewhere. */
-               res = has_masquerade_store(chan) && app_send_end_msg(app, chan);
+               res = has_masquerade_store(chan) && app_send_end_msg(control_app(control), chan);
                if (res != 0) {
                        ast_log(LOG_ERROR,
-                               "Error sending end message to %s\n", app_name);
+                               "Error sending end message to %s\n", stasis_app_name(control_app(control)));
                        return res;
                }
        } else {
@@ -1456,12 +1589,10 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
        /* The control needs to be removed from the controls container in
         * case a new PBX is started and ends up coming back into Stasis.
         */
-       ao2_cleanup(app);
-       app = NULL;
        control_unlink(control);
        control = NULL;
 
-       if (!ast_channel_pbx(chan)) {
+       if (!res && !ast_channel_pbx(chan)) {
                int chan_hungup;
 
                /* The ASYNCGOTO softhangup flag may have broken the channel out of
index 5b3b048757c052c218e843109b04069fe9d606dd..3e16e803f23182f768925bba7b507cd904a4fb9b 100644 (file)
@@ -83,9 +83,19 @@ struct stasis_app_control {
         */
        struct ast_silence_generator *silgen;
        /*!
-        * The app for which this control was created
+        * The app for which this control is currently controlling.
+        * This can change through the use of the /channels/{channelId}/move
+        * command.
         */
        struct stasis_app *app;
+       /*!
+        * The name of the next Stasis application to move to.
+        */
+       char *next_app;
+       /*!
+        * The list of arguments to pass to StasisStart when moving to another app.
+        */
+       AST_VECTOR(, char *) next_app_args;
        /*!
         * When set, /c app_stasis should exit and continue in the dialplan.
         */
@@ -101,6 +111,8 @@ static void control_dtor(void *obj)
        ast_channel_cleanup(control->channel);
        ao2_cleanup(control->app);
 
+       control_move_cleanup(control);
+
        ast_cond_destroy(&control->wait_cond);
        AST_LIST_HEAD_DESTROY(&control->add_rules);
        AST_LIST_HEAD_DESTROY(&control->remove_rules);
@@ -141,6 +153,9 @@ struct stasis_app_control *control_create(struct ast_channel *channel, struct st
                return NULL;
        }
 
+       control->next_app = NULL;
+       AST_VECTOR_INIT(&control->next_app_args, 0);
+
        return control;
 }
 
@@ -391,6 +406,73 @@ int stasis_app_control_continue(struct stasis_app_control *control, const char *
        return 0;
 }
 
+struct stasis_app_control_move_data {
+       char *app_name;
+       char *app_args;
+};
+
+static int app_control_move(struct stasis_app_control *control,
+       struct ast_channel *chan, void *data)
+{
+       struct stasis_app_control_move_data *move_data = data;
+
+       control->next_app = ast_strdup(move_data->app_name);
+       if (!control->next_app) {
+               ast_log(LOG_ERROR, "Allocation failed for next app\n");
+               return -1;
+       }
+
+       if (move_data->app_args) {
+               char *token;
+
+               while ((token = strtok_r(move_data->app_args, ",", &move_data->app_args))) {
+                       int res;
+                       char *arg;
+
+                       if (!(arg = ast_strdup(token))) {
+                               ast_log(LOG_ERROR, "Allocation failed for next app arg\n");
+                               control_move_cleanup(control);
+                               return -1;
+                       }
+
+                       res = AST_VECTOR_APPEND(&control->next_app_args, arg);
+                       if (res) {
+                               ast_log(LOG_ERROR, "Failed to append arg to next app args\n");
+                               ast_free(arg);
+                               control_move_cleanup(control);
+                               return -1;
+                       }
+               }
+       }
+
+       return 0;
+}
+
+int stasis_app_control_move(struct stasis_app_control *control, const char *app_name, const char *app_args)
+{
+       struct stasis_app_control_move_data *move_data;
+       size_t size;
+
+       size = sizeof(*move_data) + strlen(app_name) + strlen(app_args) + 2;
+       if (!(move_data = ast_calloc(1, size))) {
+               return -1;
+       }
+
+       move_data->app_name = (char *)move_data + sizeof(*move_data);
+       move_data->app_args = move_data->app_name + strlen(app_name) + 1;
+
+       strcpy(move_data->app_name, app_name); /* Safe */
+       if (app_args) {
+               strcpy(move_data->app_args, app_args); /* Safe */
+       } else {
+               move_data->app_args = NULL;
+       }
+
+       stasis_app_send_command_async(control, app_control_move, move_data, ast_free_ptr);
+
+       return 0;
+}
+
 static int app_control_redirect(struct stasis_app_control *control,
        struct ast_channel *chan, void *data)
 {
@@ -1575,3 +1657,32 @@ void stasis_app_control_shutdown(void)
        }
        ast_mutex_unlock(&dial_bridge_lock);
 }
+
+void control_set_app(struct stasis_app_control *control, struct stasis_app *app)
+{
+       ao2_cleanup(control->app);
+       control->app = ao2_bump(app);
+}
+
+char *control_next_app(struct stasis_app_control *control)
+{
+       return control->next_app;
+}
+
+void control_move_cleanup(struct stasis_app_control *control)
+{
+       ast_free(control->next_app);
+       control->next_app = NULL;
+
+       AST_VECTOR_RESET(&control->next_app_args, ast_free_ptr);
+}
+
+char **control_next_app_args(struct stasis_app_control *control)
+{
+       return AST_VECTOR_STEAL_ELEMENTS(&control->next_app_args);
+}
+
+int control_next_app_args_size(struct stasis_app_control *control)
+{
+       return AST_VECTOR_SIZE(&control->next_app_args);
+}
index 868a8091bb4014bbcf9067f44e4b8929cdb40849..67aa3b70fe456e07c077db458284f22a995324be 100644 (file)
@@ -107,6 +107,58 @@ int control_prestart_dispatch_all(struct stasis_app_control *control,
  */
 struct stasis_app *control_app(struct stasis_app_control *control);
 
+/*!
+ * \brief Set the application the control object belongs to
+ *
+ * \param control The control for the channel
+ * \param app The application this control will now belong to
+ *
+ * \note This will unref control's previous app by 1, and bump app by 1
+ */
+void control_set_app(struct stasis_app_control *control, struct stasis_app *app);
+
+/*!
+ * \brief Returns the name of the application we are moving to
+ *
+ * \param control The control for the channel
+ *
+ * \return The name of the application we are moving to
+ */
+char *control_next_app(struct stasis_app_control *control);
+
+/*!
+ * \brief Free any memory that was allocated for switching applications via
+ * /channels/{channelId}/move
+ *
+ * \param control The control for the channel
+ */
+void control_move_cleanup(struct stasis_app_control *control);
+
+/*!
+ * \brief Returns the list of arguments to pass to the application we are moving to
+ *
+ * \note If you wish to get the size of the list, control_next_app_args_size should be
+ * called before this, as this function will steal the elements from the string vector
+ * and set the size to 0.
+ *
+ * \param control The control for the channel
+ *
+ * \return The arguments to pass to the application we are moving to
+ */
+char **control_next_app_args(struct stasis_app_control *control);
+
+/*!
+ * \brief Returns the number of arguments to be passed to the application we are moving to
+ *
+ * \note This should always be called before control_next_app_args, as calling that function
+ * will steal all elements from the string vector and set the size to 0.
+ *
+ * \param control The control for the channel
+ *
+ * \return The number of arguments to be passed to the application we are moving to
+ */
+int control_next_app_args_size(struct stasis_app_control *control);
+
 /*!
  * \brief Command callback for adding a channel to a bridge
  *
index 08db22467a5615a28abe24f6bcf3f7acc010a510..616193421efba295186241af23bcbfbc2cde9d06 100644 (file)
                                }
                        ]
                },
+               {
+                       "path": "/channels/{channelId}/move",
+                       "description": "Move the channel from one Stasis application to another.",
+                       "operations": [
+                               {
+                                       "httpMethod": "POST",
+                                       "summary": "Move the channel from one Stasis application to another.",
+                                       "nickname": "move",
+                                       "responseClass": "void",
+                                       "parameters": [
+                                               {
+                                                       "name": "channelId",
+                                                       "description": "Channel's id",
+                                                       "paramType": "path",
+                                                       "required": true,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               },
+                                               {
+                                                       "name": "app",
+                                                       "description": "The channel will be passed to this Stasis application.",
+                                                       "paramType": "query",
+                                                       "required": true,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               },
+                                               {
+                                                       "name": "appArgs",
+                                                       "description": "The application arguments to pass to the Stasis application provided by 'app'.",
+                                                       "paramType": "query",
+                                                       "required": false,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": "404",
+                                                       "reason": "Channel not found"
+                                               },
+                                               {
+                                                       "code": "409",
+                                                       "reason": "Channel not in a Stasis application"
+                                               }
+                                       ]
+                               }
+                       ]
+               },
                {
                        "path": "/channels/{channelId}/redirect",
                        "description": "Inform the channel that it should redirect itself to a different location. Note that this will almost certainly cause the channel to exit the application.",
index d85d8d9fe4fabde2b3bcd6cbc36655cb0b2316eb..c9f4b6ae422fb4f2a872f3cb991c074a83e1a283 100644 (file)
                                "RecordingStarted",
                                "RecordingFinished",
                                "RecordingFailed",
+                               "ApplicationMoveFailed",
                                "ApplicationReplaced",
                                "BridgeCreated",
                                "BridgeDestroyed",
                                }
                        }
                },
+               "ApplicationMoveFailed": {
+                       "id": "ApplicationMoveFailed",
+                       "description": "Notification that trying to move a channel to another Stasis application failed.",
+                       "properties": {
+                               "channel": {
+                                       "required": true,
+                                       "type": "Channel"
+                               },
+                               "destination": {
+                                       "required": true,
+                                       "type": "string"
+                               },
+                               "args": {
+                                       "required": true,
+                                       "type": "List[string]",
+                                       "description": "Arguments to the application"
+                               }
+                       }
+               },
                "ApplicationReplaced": {
                        "id": "ApplicationReplaced",
                        "description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.",