[Asterisk-code-review] res stasis: Add ability to switch applications. (asterisk[16])

Benjamin Keith Ford asteriskteam at digium.com
Tue Feb 5 13:11:14 CST 2019


Benjamin Keith Ford has uploaded this change for review. ( https://gerrit.asterisk.org/10960


Change subject: res_stasis: Add ability to switch applications.
......................................................................

res_stasis: Add ability to switch applications.

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
---
M CHANGES
M include/asterisk/stasis_app.h
M res/ari/ari_model_validators.c
M res/ari/ari_model_validators.h
M res/ari/resource_channels.c
M res/ari/resource_channels.h
M res/res_ari_channels.c
M res/res_stasis.c
M res/stasis/control.c
M res/stasis/control.h
M rest-api/api-docs/channels.json
M rest-api/api-docs/events.json
12 files changed, 707 insertions(+), 27 deletions(-)



  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/60/10960/1

diff --git a/CHANGES b/CHANGES
index f5deca9..3c3b39d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -20,6 +20,20 @@
    extension, and the 'h' extension. Any phone that registers under this context
    will place all calls to the corresponding Stasis application.
 
+ * 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
 ------------------
  * Added "send_contact_status_on_update_registration" global configuration option
diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h
index b0829ab..3dd23db 100644
--- a/include/asterisk/stasis_app.h
+++ b/include/asterisk/stasis_app.h
@@ -491,6 +491,20 @@
 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
diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c
index a06a1f5..6aff552 100644
--- a/res/ari/ari_model_validators.c
+++ b/res/ari/ari_model_validators.c
@@ -2028,6 +2028,127 @@
 	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 @@
 	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 @@
 	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
diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h
index ab0b2f6..0471631 100644
--- a/res/ari/ari_model_validators.h
+++ b/res/ari/ari_model_validators.h
@@ -624,6 +624,24 @@
 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 @@
  * - 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)
diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c
index 8aeb096..c256050 100644
--- a/res/ari/resource_channels.c
+++ b/res/ari/resource_channels.c
@@ -217,6 +217,26 @@
 	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)
diff --git a/res/ari/resource_channels.h b/res/ari/resource_channels.h
index b071d08..fdd7a6b 100644
--- a/res/ari/resource_channels.h
+++ b/res/ari/resource_channels.h
@@ -261,6 +261,34 @@
  * \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 */
diff --git a/res/res_ari_channels.c b/res/res_ari_channels.c
index dae146c..3d96d60 100644
--- a/res/res_ari_channels.c
+++ b/res/res_ari_channels.c
@@ -778,6 +778,95 @@
 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 @@
 	.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 @@
 		[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 = {
diff --git a/res/res_stasis.c b/res/res_stasis.c
index 704d779..def0cb3 100644
--- a/res/res_stasis.c
+++ b/res/res_stasis.c
@@ -1293,7 +1293,8 @@
 int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc,
 		    char *argv[])
 {
-	RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup);
+	SCOPED_MODULE_USE(ast_module_info->self);
+
 	RAII_VAR(struct stasis_app_control *, control, NULL, control_unlink);
 	struct ast_bridge *bridge = NULL;
 	int res = 0;
@@ -1310,21 +1311,19 @@
 		return -1;
 	}
 
-	app = ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY);
-	if (!app) {
-		ast_log(LOG_ERROR,
-			"Stasis app '%s' not registered\n", app_name);
-		return -1;
-	}
-	if (!app_is_active(app)) {
-		ast_log(LOG_ERROR,
-			"Stasis app '%s' not active\n", app_name);
+	control = control_create(chan, ao2_find(apps_registry, app_name, OBJ_SEARCH_KEY));
+	if (!control) {
+		ast_log(LOG_ERROR, "Control allocation failed or Stasis app '%s' not registered\n", app_name);
 		return -1;
 	}
 
-	control = control_create(chan, app);
-	if (!control) {
-		ast_log(LOG_ERROR, "Allocated failed\n");
+	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 +1333,7 @@
 		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 +1356,114 @@
 			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)) {
+
+				/* 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);
+						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);
+					break;
+				}
+
+				res = send_start_msg(control_app(control), chan, control_argc(control), control_argv(control));
+				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);
+					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;
+
+					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 < control_argc(control); ++idx) {
+							r = ast_json_array_append(json_args,
+								ast_json_string_create(control_argv(control)[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);
+		}
+
 		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 +1523,18 @@
 	}
 
 	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 +1554,10 @@
 	/* 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
diff --git a/res/stasis/control.c b/res/stasis/control.c
index e4d007c..e897aec 100644
--- a/res/stasis/control.c
+++ b/res/stasis/control.c
@@ -83,10 +83,24 @@
 	 */
 	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.
+	 */
+	char **argv;
+	/*!
+	 * The number of arguments argv contains.
+	 */
+	int argc;
+	/*!
 	 * When set, /c app_stasis should exit and continue in the dialplan.
 	 */
 	int is_done:1;
@@ -101,6 +115,8 @@
 	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);
@@ -125,11 +141,12 @@
 	if (res != 0) {
 		ast_log(LOG_ERROR, "Error initializing ast_cond_t: %s\n",
 			strerror(errno));
+		ao2_cleanup(app);
 		ao2_ref(control, -1);
 		return NULL;
 	}
 
-	control->app = ao2_bump(app);
+	control->app = app;
 
 	ast_channel_ref(channel);
 	control->channel = channel;
@@ -141,6 +158,10 @@
 		return NULL;
 	}
 
+	control->next_app = NULL;
+	control->argv = NULL;
+	control->argc = 0;
+
 	return control;
 }
 
@@ -391,6 +412,103 @@
 	return 0;
 }
 
+struct stasis_app_control_move_data {
+	char *app_name;
+	char **argv;
+	int argc;
+};
+
+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;
+	size_t size;
+	int idx;
+
+	size = strlen(move_data->app_name) * sizeof(*move_data->app_name) + 1;
+	if (!(control->next_app = ast_calloc(1, size))) {
+		ast_log(LOG_ERROR, "Allocation failed for next app\n");
+		return -1;
+	}
+
+	ast_copy_string(control->next_app, S_OR(move_data->app_name, ""), size);
+	control->argc = move_data->argc;
+
+	if (move_data->argv) {
+		size = sizeof(*move_data->argv) * control->argc;
+		if (!(control->argv = ast_calloc(1, size))) {
+			ast_log(LOG_ERROR, "Allocation failed for argument list\n");
+			control_move_cleanup(control);
+			return -1;
+		}
+
+		for (idx = 0; idx < control->argc; idx++) {
+			size = strlen(move_data->argv[idx]) * sizeof(*move_data->argv[idx]) + 1;
+			if (!(control->argv[idx] = ast_calloc(1, size))) {
+				ast_log(LOG_ERROR, "Allocation failed for argument\n");
+				control_move_cleanup(control);
+				return -1;
+			}
+			ast_copy_string(control->argv[idx], S_OR(move_data->argv[idx], ""), size);
+		}
+	} else {
+		control->argv = NULL;
+	}
+
+	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;
+	int argc = 0;
+	char *iter;
+	char *token;
+
+	if (!(move_data = ast_calloc(1, sizeof(*move_data)))) {
+		return -1;
+	}
+
+	size = strlen(app_name) * sizeof(*app_name) + 1;
+	if (!(move_data->app_name = ast_calloc(1, size))) {
+		return -1;
+	}
+
+	ast_copy_string(move_data->app_name, S_OR(app_name, ""), size);
+
+	if (app_args) {
+		size = strlen(app_args) * sizeof(*app_args) + 1;
+		if (!(iter = ast_calloc(1, size))) {
+			return -1;
+		}
+
+		ast_copy_string(iter, S_OR(app_args, ""), size);
+
+		if (!(move_data->argv = ast_calloc(1, sizeof(*move_data->argv)))) {
+			return -1;
+		}
+
+		while ((token = strtok_r(iter, ",", &iter))) {
+			if (!(move_data->argv = ast_realloc(move_data->argv, sizeof(*move_data->argv) * (argc + 1)))) {
+				return -1;
+			}
+			size = strlen(token) * sizeof(*token) + 1;
+			move_data->argv[argc] = ast_calloc(1, size);
+			ast_copy_string(move_data->argv[argc], S_OR(token, ""), size);
+			argc++;
+		}
+	} else {
+		move_data->argv = NULL;
+	}
+
+	move_data->argc = argc;
+
+	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)
 {
@@ -1590,3 +1708,47 @@
 	}
 	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)
+{
+	if (control->next_app) {
+		ast_free(control->next_app);
+	}
+	control->next_app = NULL;
+
+	if (control->argv) {
+		int idx;
+
+		for (idx = 0; idx < control->argc; idx++) {
+			if (control->argv[idx]) {
+				ast_free(control->argv[idx]);
+			}
+		}
+
+		ast_free(control->argv);
+	}
+	control->argv = NULL;
+
+	control->argc = 0;
+}
+
+char **control_argv(struct stasis_app_control *control)
+{
+	return control->argv;
+}
+
+int control_argc(struct stasis_app_control *control)
+{
+	return control->argc;
+}
diff --git a/res/stasis/control.h b/res/stasis/control.h
index 868a809..86b0c4a 100644
--- a/res/stasis/control.h
+++ b/res/stasis/control.h
@@ -37,6 +37,9 @@
  *
  * \return New control object.
  * \return \c NULL on error.
+ *
+ * \note This function inherits app's ref rather than bumping app!
+ * If something goes wrong, ao2_cleanup is called on the app.
  */
 struct stasis_app_control *control_create(struct ast_channel *channel, struct stasis_app *app);
 
@@ -108,6 +111,30 @@
 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 the previous app by 1, and bump app by 1
+ */
+void control_set_app(struct stasis_app_control *control, struct stasis_app *app);
+
+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);
+
+char **control_argv(struct stasis_app_control *control);
+
+int control_argc(struct stasis_app_control *control);
+
+/*!
  * \brief Command callback for adding a channel to a bridge
  *
  * \param control The control for chan
diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json
index 08db224..6161934 100644
--- a/rest-api/api-docs/channels.json
+++ b/rest-api/api-docs/channels.json
@@ -516,6 +516,54 @@
 			]
 		},
 		{
+			"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": [
diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json
index d85d8d9..c9f4b6a 100644
--- a/rest-api/api-docs/events.json
+++ b/rest-api/api-docs/events.json
@@ -159,6 +159,7 @@
 				"RecordingStarted",
 				"RecordingFinished",
 				"RecordingFailed",
+				"ApplicationMoveFailed",
 				"ApplicationReplaced",
 				"BridgeCreated",
 				"BridgeDestroyed",
@@ -335,6 +336,25 @@
 				}
 			}
 		},
+		"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.",

-- 
To view, visit https://gerrit.asterisk.org/10960
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: 16
Gerrit-MessageType: newchange
Gerrit-Change-Id: I43d12b10045a98a8d42541889b85695be26f288a
Gerrit-Change-Number: 10960
Gerrit-PatchSet: 1
Gerrit-Owner: Benjamin Keith Ford <bford at digium.com>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20190205/0edb63a3/attachment-0001.html>


More information about the asterisk-code-review mailing list