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 d347507..60d8a4d 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 c40e901..01c7ff4 100644 (file)
@@ -502,6 +502,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
  *
  * \param control Control for \c res_stasis
index 44d9d77..254de3c 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 1ee74f4..4b3269a 100644 (file)
@@ -624,6 +624,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.
  *
  * Notification that another WebSocket has taken over for an application.
@@ -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 08f97f1..eca70ce 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 b071d08..fdd7a6b 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 dae146c..3d96d60 100644 (file)
@@ -778,6 +778,95 @@ static void ast_ari_channels_continue_in_dialplan_cb(
 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;
+}
 int ast_ari_channels_redirect_parse_body(
        struct ast_json *body,
        struct ast_ari_channels_redirect_args *args)
@@ -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 4da78a6..4b7c3ed 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 5b3b048..3e16e80 100644 (file)
@@ -83,10 +83,20 @@ 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.
         */
        int is_done:1;
@@ -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 868a809..67aa3b7 100644 (file)
@@ -108,6 +108,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
  *
  * \param control The control for chan
index 08db224..6161934 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.",
                        "operations": [
index d85d8d9..c9f4b6a 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.",