[Asterisk-code-review] pbx: Add CLI commands to warn of bad branches or audio playback (asterisk[master])

N A asteriskteam at digium.com
Sun Jan 2 19:07:12 CST 2022


N A has uploaded this change for review. ( https://gerrit.asterisk.org/c/asterisk/+/17719 )


Change subject: pbx: Add CLI commands to warn of bad branches or audio playback
......................................................................

pbx: Add CLI commands to warn of bad branches or audio playback

Adds two dialplan CLI commands to scan the dialplan preemptively
for common runtime problems.

'dialplan analyze fallthrough' looks through branching applications
for attempted branches to nonexistent dialplan locations.

'dialplan analyze audio' looks through audio applications
for attempted playback of nonexistent audio files.

These will not find all possible problems but any problems found
would cause an error at runtime. This allows users to preemptively
find these problems before they occur.

ASTERISK-29828 #close

Change-Id: Iaf7a2c8a6982ee5adaae7f7aa63e311d0e9106e3
---
A doc/CHANGES-staging/pbx-cli.txt
M main/pbx.c
2 files changed, 460 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/19/17719/1

diff --git a/doc/CHANGES-staging/pbx-cli.txt b/doc/CHANGES-staging/pbx-cli.txt
new file mode 100644
index 0000000..662d1d1
--- /dev/null
+++ b/doc/CHANGES-staging/pbx-cli.txt
@@ -0,0 +1,6 @@
+Subject: Add CLI commands to preemptively find runtime errors
+
+Adds the 'dialplan analyze fallthrough' and 'dialplan analyze
+audio' commands to scan the dialplan for branches to
+nonexistent dialplan locations and attempted use of nonexistent
+audio files.
diff --git a/main/pbx.c b/main/pbx.c
index 07cf8e7..6bdb4df 100644
--- a/main/pbx.c
+++ b/main/pbx.c
@@ -5666,6 +5666,325 @@
 	return (dpc->total_exten == old_total_exten) ? -1 : res;
 }
 
+static int pbx_parseable_location(char **label, char **context, char **exten, char **pri, int *ipri)
+{
+	*context = *exten = *pri = NULL;
+	/* Split context,exten,pri */
+	*context = strsep(label, ",");
+	*exten = strsep(label, ",");
+	*pri = strsep(label, ",");
+	if (!*exten) {
+		/* Only a priority in this one */
+		*pri = *context;
+		*exten = NULL;
+		*context = NULL;
+	} else if (!*pri) {
+		/* Only an extension and priority in this one */
+		*pri = *exten;
+		*exten = *context;
+		*context = NULL;
+	}
+	*ipri = atoi(*pri);
+	return 0;
+}
+
+/*!
+ * \internal
+ * \brief Checks if a dialplan location exists
+ *
+ * \retval 0 location exists
+ * \retval -1 location does not exist
+ */
+static int dialplan_location_exists(char *context, struct ast_context *c, char *exten, struct ast_exten *e, char *pri, int ipri)
+{
+	if (!context) {
+		context = (char*) ast_get_context_name(c); /* assume current context */
+	}
+	if (!exten) {
+		exten = (char*) ast_get_extension_name(e); /* assume current extension */
+	}
+	ast_debug(8, "Scrutinizing %s:%d %s: %s,%s,%s\n", ast_get_extension_registrar_file(e), ast_get_extension_registrar_line(e),
+		ast_get_extension_app(e), context, exten, pri);
+	if (ipri && ast_exists_extension(NULL, context, exten, ipri, e->matchcid == AST_EXT_MATCHCID_ON ? e->cidmatch : NULL)) {
+		return 0;
+	} else if (ast_findlabel_extension(NULL, context, exten, pri, e->matchcid == AST_EXT_MATCHCID_ON ? e->cidmatch : NULL) > 0) {
+		return 0;
+	}
+	return -1;
+}
+
+/*!
+ * \internal
+ * \brief See if a dialplan extension change will fall through
+ *
+ * \note A successful return does not necessarily mean a fallthrough will not happen.
+ *	This function should not return false positives, but there will be false negatives.
+ *
+ * \param c context
+ * \param e exten
+ *
+ * \retval 0 indeterminate
+ * \retval -1 fallthrough
+ */
+static int dialplan_find_fallthrough(struct ast_context *c, struct ast_exten *e)
+{
+	char *context, *exten, *pri;
+	char *data, *sep;
+	const char *app = ast_get_extension_app(e);
+	int ipri;
+
+	if (strcasecmp(app, "Goto") && strcasecmp(app, "GotoIf") && strcasecmp(app, "Gosub") && strcasecmp(app, "GosubIf")) {
+		return 0; /* no location change */
+	}
+	/* in theory, ChannelRedirect is another easy one to handle, but 99.99% of the time, channel will be a variable... */
+
+	data = ast_strdupa(ast_get_extension_app_data(e));
+	if (ast_strlen_zero(data)) {
+		return -1;
+	}
+	if (!strcasecmp(app, "Goto")) {
+		if (strchr(data, '$')) {
+			return 0; /* if location contains a variable, there's no real way to know at "compile" time, gotta hope for the best at runtime... */
+		}
+		pbx_parseable_location(&data, &context, &exten, &pri, &ipri);
+		return dialplan_location_exists(context, c, exten, e, pri, ipri);
+	}
+	if (!strcasecmp(app, "GotoIf")) {
+		if (strchr(data, '$')) {
+			/* in theory, could parse the condition and each branch separately, but we'd need to find the right ? and :
+				This is more difficult than actual GotoIf/GosubIf because data isn't variable-substituted at all in advance,
+				nor can we substitute it. */
+			return 0;
+		}
+		data = strchr(data, '?');
+		if (!data) {
+			return -1; /* this shouldn't happen, so *something* is wrong... */
+		}
+		data++;
+		/* because we already bailed out early if a $ was detected, it is guaranteed that the first : will be the separator */
+		sep = strchr(data, ':');
+		if (sep) {
+			sep[0] = '\0';
+			sep++;
+			if (*sep) { /* make sure this branch contains data */
+				pbx_parseable_location(&sep, &context, &exten, &pri, &ipri);
+				if (dialplan_location_exists(context, c, exten, e, pri, ipri)) {
+					return -1;
+				}
+			}
+		}
+		if (!*data) { /* make sure this branch contains data */
+			return 0; /* if location contains a variable, there's no real way to know at "compile" time, gotta hope for the best at runtime... */
+		}
+		pbx_parseable_location(&data, &context, &exten, &pri, &ipri);
+		return dialplan_location_exists(context, c, exten, e, pri, ipri);
+	}
+	if (!strcasecmp(app, "Gosub")) {
+		if (strchr(data, '$')) {
+			return 0; /* if location contains a variable, there's no real way to know at "compile" time, gotta hope for the best at runtime... */
+		}
+		sep = strchr(data, '('); /* start of arguments */
+		if (sep) {
+			sep[0] = '\0'; /* discard Gosub arguments */
+		}
+		pbx_parseable_location(&data, &context, &exten, &pri, &ipri);
+		return dialplan_location_exists(context, c, exten, e, pri, ipri);
+	}
+	if (!strcasecmp(app, "GosubIf")) {
+		if (strchr(data, '$')) {
+			return 0; /* in theory, could parse the condition and each branch separately, but we'd need to find the right ? and : */
+		}
+		data = strchr(data, '?');
+		if (!data) {
+			return -1; /* this shouldn't happen, so *something* is wrong... */
+		}
+		data++;
+		/* because we already bailed out early if a $ was detected, it is guaranteed that the first : will be the separator */
+		sep = strchr(data, ':');
+		if (sep) {
+			sep[0] = '\0';
+			sep++;
+			if (*sep) { /* make sure this branch contains data */
+				char *argstart = strchr(data, '(');
+				if (argstart) {
+					argstart[0] = '\0'; /* discard Gosub arguments */
+				}
+				pbx_parseable_location(&sep, &context, &exten, &pri, &ipri);
+				if (dialplan_location_exists(context, c, exten, e, pri, ipri)) {
+					return -1;
+				}
+			}
+		}
+		if (!*data) { /* make sure this branch contains data */
+			return 0; /* if location contains a variable, there's no real way to know at "compile" time, gotta hope for the best at runtime... */
+		}
+		sep = strchr(data, '('); /* start of arguments */
+		if (sep) {
+			sep[0] = '\0'; /* discard Gosub arguments */
+		}
+		pbx_parseable_location(&data, &context, &exten, &pri, &ipri);
+		return dialplan_location_exists(context, c, exten, e, pri, ipri);
+	}
+
+	return 0;
+}
+
+static int dialplan_find_fallthrough_callback(struct ast_context *c, struct ast_exten *e)
+{
+	if (dialplan_find_fallthrough(c, e)) {
+		ast_log(LOG_WARNING, "%s:%d: %s(%s) will fail (branch to nonexistent location)\n", ast_get_extension_registrar_file(e),
+		ast_get_extension_registrar_line(e), ast_get_extension_app(e), (char*) ast_get_extension_app_data(e));
+	}
+	return 0; /* always return 0, so we always crawl every priority in the dialplan */
+}
+
+static int dialplan_find_missing_audio_callback(struct ast_context *c, struct ast_exten *e)
+{
+	const char *app;
+	char *data;
+	char *cur, *audiodata;
+
+	AST_DECLARE_APP_ARGS(args,
+		AST_APP_ARG(audio);
+		AST_APP_ARG(audio2);
+		AST_APP_ARG(digits);
+		AST_APP_ARG(opts);
+		AST_APP_ARG(rest);
+	);
+
+	app = ast_get_extension_app(e);
+
+	if (strcasecmp(app, "Playback") && strcasecmp(app, "ControlPlayback") && strcasecmp(app, "Background") && strcasecmp(app, "BackgroundDetect") && strcasecmp(app, "Read") && strcasecmp(app, "ReadExten")) {
+		return 0; /* not relevant to this callback */
+	}
+
+	data = ast_strdupa(ast_get_extension_app_data(e));
+
+	AST_STANDARD_APP_ARGS(args, data);
+
+	if (ast_strlen_zero(args.audio)) { /* arg1 is not audio for Read/ReadExten, but regardless it IS a mandatory arg so this is NEVER valid */
+		ast_log(LOG_WARNING, "%s:%d: %s(%s) missing mandatory argument\n", ast_get_extension_registrar_file(e),
+		ast_get_extension_registrar_line(e), ast_get_extension_app(e), (char*) ast_get_extension_app_data(e));
+		return 0;
+	}
+
+	if (!strcasecmp(app, "Playback") || !strcasecmp(app, "ControlPlayback") || !strcasecmp(app, "Background") || !strcasecmp(app, "BackgroundDetect")) {
+		audiodata = args.audio;
+		/* by delaying this check until here, we will be able to parse more things since succeeding parameters with variables are discarded first */
+		if (strchr(audiodata, '$')) {
+			return 0; /* ignore audio args with variables, hope for the best at runtime... */
+		}
+	} else { /* Read/ReadExten */
+		audiodata = args.audio2;
+		if (ast_strlen_zero(audiodata) || strchr(audiodata, '$') || (args.audio && strchr(args.audio, '$'))) { /* if previous args contained vars, that could mess up our primitive parsing */
+			return 0; /* ignore audio args with variables, hope for the best at runtime... */
+		}
+	}
+
+	while ((cur = strsep(&audiodata, "&"))) {
+		if (!strncmp(cur, "/tmp/", 5)) {
+			continue; /* ignore tmp files, for obvious reasons... */
+		}
+		if (ast_fileexists(cur, NULL, NULL)) {
+			continue;
+		}
+		if (args.opts && strchr(args.opts, 'i')) {
+			continue; /* ignore indications tone names for Read/ReadExten */
+		}
+		ast_log(LOG_WARNING, "%s:%d: %s(%s) will fail (audio file '%s' does not exist)\n", ast_get_extension_registrar_file(e),
+			ast_get_extension_registrar_line(e), ast_get_extension_app(e), (char*) ast_get_extension_app_data(e), cur);
+	}
+	return 0; /* always return 0, so we always crawl every priority in the dialplan */
+}
+
+static int dialplan_crawl_helper(int fd, const char *context, const char *exten, struct dialplan_counters *dpc, const struct ast_include *rinclude, int includecount, const char *includes[], int (*crawl_callback)(struct ast_context *c, struct ast_exten *e))
+{
+	struct ast_context *c = NULL;
+	int res = 0, old_total_exten = dpc->total_exten;
+
+	ast_rdlock_contexts();
+
+	/* walk all contexts ... */
+	while ( (c = ast_walk_contexts(c)) ) {
+		int idx;
+		struct ast_exten *e;
+
+		if (context && strcmp(ast_get_context_name(c), context)) {
+			continue;	/* skip this one, name doesn't match */
+		}
+		dpc->context_existence = 1;
+		ast_rdlock_context(c);
+		if (!exten) {
+			dpc->total_context++;
+		}
+
+		/* walk extensions ... */
+		e = NULL;
+		while ( (e = ast_walk_context_extensions(c, e)) ) {
+			struct ast_exten *p;
+
+			if (exten && !ast_extension_match(ast_get_extension_name(e), exten)) {
+				continue;	/* skip, extension match failed */
+			}
+			dpc->extension_existence = 1;
+			dpc->total_prio++;
+
+			if (crawl_callback(c, e)) {
+				continue;
+			}
+
+			dpc->total_exten++;
+			/* walk next extension peers */
+			p = e;	/* skip the first one, we already got it */
+			while ( (p = ast_walk_extension_priorities(e, p)) ) {
+				dpc->total_prio++;
+				if (crawl_callback(c, p)) {
+					break;
+				}
+			}
+		}
+
+		for (idx = 0; idx < ast_context_includes_count(c); idx++) {
+			const struct ast_include *i = ast_context_includes_get(c, idx);
+			if (exten) {
+				/* Check all includes for the requested extension */
+				if (includecount >= AST_PBX_MAX_STACK) {
+					ast_log(LOG_WARNING, "Maximum include depth exceeded!\n");
+				} else {
+					int dupe = 0;
+					int x;
+					for (x = 0; x < includecount; x++) {
+						if (!strcasecmp(includes[x], ast_get_include_name(i))) {
+							dupe++;
+							break;
+						}
+					}
+					if (!dupe) {
+						includes[includecount] = ast_get_include_name(i);
+						dialplan_crawl_helper(fd, ast_get_include_name(i), exten, dpc, i, includecount + 1, includes, crawl_callback);
+					} else {
+						ast_log(LOG_WARNING, "Avoiding circular include of %s within %s\n", ast_get_include_name(i), context);
+					}
+				}
+			}
+		}
+		ast_unlock_context(c);
+	}
+	ast_unlock_contexts();
+
+	return (dpc->total_exten == old_total_exten) ? -1 : res;
+}
+
+static int dialplan_analyze_fallthroughs(int fd, const char *context, const char *exten, struct dialplan_counters *dpc, const struct ast_include *rinclude, int includecount, const char *includes[])
+{
+	return dialplan_crawl_helper(fd, context, exten, dpc, rinclude, includecount, includes, dialplan_find_fallthrough_callback);
+}
+
+static int dialplan_analyze_audio(int fd, const char *context, const char *exten, struct dialplan_counters *dpc, const struct ast_include *rinclude, int includecount, const char *includes[])
+{
+	return dialplan_crawl_helper(fd, context, exten, dpc, rinclude, includecount, includes, dialplan_find_missing_audio_callback);
+}
+
 static int show_debug_helper(int fd, const char *context, const char *exten, struct dialplan_counters *dpc, struct ast_include *rinclude, int includecount, const char *includes[])
 {
 	struct ast_context *c = NULL;
@@ -5787,6 +6106,138 @@
 	return CLI_SUCCESS;
 }
 
+static char *handle_analyze_fallthrough(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	char *exten = NULL, *context = NULL;
+	/* Variables used for different counters */
+	struct dialplan_counters counters;
+	const char *incstack[AST_PBX_MAX_STACK];
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "dialplan analyze fallthrough";
+		e->usage =
+			"Usage: dialplan analyze fallthrough [[exten@]context]\n"
+			"       Analyzes dialplan for extension fallthroughs\n";
+		return NULL;
+	case CLI_GENERATE:
+		return complete_show_dialplan_context(a->line, a->word, a->pos, a->n);
+	}
+
+	memset(&counters, 0, sizeof(counters));
+
+	if (a->argc != 3 && a->argc != 4)
+		return CLI_SHOWUSAGE;
+
+	/* we obtain [exten@]context? if yes, split them ... */
+	if (a->argc == 4) {
+		if (strchr(a->argv[3], '@')) {	/* split into exten & context */
+			context = ast_strdupa(a->argv[3]);
+			exten = strsep(&context, "@");
+			/* change empty strings to NULL */
+			if (ast_strlen_zero(exten))
+				exten = NULL;
+		} else { /* no '@' char, only context given */
+			context = ast_strdupa(a->argv[3]);
+		}
+		if (ast_strlen_zero(context))
+			context = NULL;
+	}
+	/* else Show complete dial plan, context and exten are NULL */
+	dialplan_analyze_fallthroughs(a->fd, context, exten, &counters, NULL, 0, incstack);
+
+	/* check for input failure and throw some error messages */
+	if (context && !counters.context_existence) {
+		ast_cli(a->fd, "There is no existence of '%s' context\n", context);
+		return CLI_FAILURE;
+	}
+
+	if (exten && !counters.extension_existence) {
+		if (context)
+			ast_cli(a->fd, "There is no existence of %s@%s extension\n",
+				exten, context);
+		else
+			ast_cli(a->fd,
+				"There is no existence of '%s' extension in all contexts\n",
+				exten);
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd,"-= %d %s (%d %s) in %d %s. =-\n",
+		counters.total_exten, counters.total_exten == 1 ? "extension" : "extensions",
+		counters.total_prio, counters.total_prio == 1 ? "priority" : "priorities",
+		counters.total_context, counters.total_context == 1 ? "context" : "contexts");
+
+	/* everything ok */
+	return CLI_SUCCESS;
+}
+
+static char *handle_analyze_audio(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	char *exten = NULL, *context = NULL;
+	/* Variables used for different counters */
+	struct dialplan_counters counters;
+	const char *incstack[AST_PBX_MAX_STACK];
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "dialplan analyze audio";
+		e->usage =
+			"Usage: dialplan analyze audio [[exten@]context]\n"
+			"       Analyzes dialplan for missing audio files\n";
+		return NULL;
+	case CLI_GENERATE:
+		return complete_show_dialplan_context(a->line, a->word, a->pos, a->n);
+	}
+
+	memset(&counters, 0, sizeof(counters));
+
+	if (a->argc != 3 && a->argc != 4)
+		return CLI_SHOWUSAGE;
+
+	/* we obtain [exten@]context? if yes, split them ... */
+	if (a->argc == 4) {
+		if (strchr(a->argv[3], '@')) {	/* split into exten & context */
+			context = ast_strdupa(a->argv[3]);
+			exten = strsep(&context, "@");
+			/* change empty strings to NULL */
+			if (ast_strlen_zero(exten))
+				exten = NULL;
+		} else { /* no '@' char, only context given */
+			context = ast_strdupa(a->argv[3]);
+		}
+		if (ast_strlen_zero(context))
+			context = NULL;
+	}
+	/* else Show complete dial plan, context and exten are NULL */
+	dialplan_analyze_audio(a->fd, context, exten, &counters, NULL, 0, incstack);
+
+	/* check for input failure and throw some error messages */
+	if (context && !counters.context_existence) {
+		ast_cli(a->fd, "There is no existence of '%s' context\n", context);
+		return CLI_FAILURE;
+	}
+
+	if (exten && !counters.extension_existence) {
+		if (context)
+			ast_cli(a->fd, "There is no existence of %s@%s extension\n",
+				exten, context);
+		else
+			ast_cli(a->fd,
+				"There is no existence of '%s' extension in all contexts\n",
+				exten);
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd,"-= %d %s (%d %s) in %d %s. =-\n",
+		counters.total_exten, counters.total_exten == 1 ? "extension" : "extensions",
+		counters.total_prio, counters.total_prio == 1 ? "priority" : "priorities",
+		counters.total_context, counters.total_context == 1 ? "context" : "contexts");
+
+	/* everything ok */
+	return CLI_SUCCESS;
+}
+
 /*! \brief Send ack once */
 static char *handle_debug_dialplan(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
 {
@@ -6163,6 +6614,8 @@
 	AST_CLI_DEFINE(handle_show_device2extenstate, "Show expected exten state from multiple device states"),
 #endif
 	AST_CLI_DEFINE(handle_show_dialplan, "Show dialplan"),
+	AST_CLI_DEFINE(handle_analyze_fallthrough, "Analyzes dialplan for extension fallthroughs"),
+	AST_CLI_DEFINE(handle_analyze_audio, "Analyzes dialplan for missing referenced audio files"),
 	AST_CLI_DEFINE(handle_debug_dialplan, "Show fast extension pattern matching data structures"),
 	AST_CLI_DEFINE(handle_unset_extenpatternmatchnew, "Use the Old extension pattern matching algorithm."),
 	AST_CLI_DEFINE(handle_set_extenpatternmatchnew, "Use the New extension pattern matching algorithm."),
@@ -9033,3 +9486,4 @@
 
 	return (hints && hintdevices && autohints && statecbs) ? 0 : -1;
 }
+

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

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Change-Id: Iaf7a2c8a6982ee5adaae7f7aa63e311d0e9106e3
Gerrit-Change-Number: 17719
Gerrit-PatchSet: 1
Gerrit-Owner: N A <mail at interlinked.x10host.com>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20220102/0ea86d69/attachment-0001.html>


More information about the asterisk-code-review mailing list