[asterisk-commits] tilghman: trunk r117725 - in /trunk: ./ apps/ doc/

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Thu May 22 00:10:03 CDT 2008


Author: tilghman
Date: Thu May 22 00:10:01 2008
New Revision: 117725

URL: http://svn.digium.com/view/asterisk?view=rev&rev=117725
Log:
Enhance ExternalIVR with new options and commands.
(closes issue #12705)
 Reported by: ctooley
 Patches: 
       new_externalivr_argument_format-v2.diff uploaded by ctooley (license 136)
       new_externalivr_documentation.diff uploaded by ctooley (license 136)
       and a few additional fixes by me

Modified:
    trunk/CHANGES
    trunk/apps/app_externalivr.c
    trunk/doc/externalivr.txt

Modified: trunk/CHANGES
URL: http://svn.digium.com/view/asterisk/trunk/CHANGES?view=diff&rev=117725&r1=117724&r2=117725
==============================================================================
--- trunk/CHANGES (original)
+++ trunk/CHANGES Thu May 22 00:10:01 2008
@@ -72,6 +72,9 @@
    words, if using the 'd' option, it is not possible to enter a number to append to
    the first argument to Chanspy(). Pressing 4 will change to spy mode, pressing 5 will
    change to whisper mode, and pressing 6 will change to barge mode.
+ * ExternalIVR now takes several options that affect the way it performs, as
+   well as having several new commands.  Please see doc/externalivr.txt for the
+   complete documentation.
 
 SIP Changes
 -----------

Modified: trunk/apps/app_externalivr.c
URL: http://svn.digium.com/view/asterisk/trunk/apps/app_externalivr.c?view=diff&rev=117725&r1=117724&r2=117725
==============================================================================
--- trunk/apps/app_externalivr.c (original)
+++ trunk/apps/app_externalivr.c Thu May 22 00:10:01 2008
@@ -50,19 +50,37 @@
 static const char *app = "ExternalIVR";
 
 static const char *synopsis = "Interfaces with an external IVR application";
-
 static const char *descrip =
-"  ExternalIVR(command|ivr://ivrhost[,arg[,arg...]]): Either forks a process\n"
+"  ExternalIVR(command|ivr://ivrhosti([,arg[,arg...]])[,options]): Either forks a process\n"
 "to run given command or makes a socket to connect to given host and starts\n"
 "a generator on the channel. The generator's play list is controlled by the\n"
 "external application, which can add and clear entries via simple commands\n"
 "issued over its stdout. The external application will receive all DTMF events\n"
 "received on the channel, and notification if the channel is hung up. The\n"
 "application will not be forcibly terminated when the channel is hung up.\n"
-"See doc/externalivr.txt for a protocol specification.\n";
+"See doc/externalivr.txt for a protocol specification.\n"
+"The 'n' option tells ExternalIVR() not to answer the channel. \n"
+"The 'i' option tells ExternalIVR() not to send a hangup and exit when the\n"
+"  channel receives a hangup, instead it sends an 'I' informative message\n"
+"  meaning that the external application MUST hang up the call with an H command\n"
+"The 'd' option tells ExternalIVR() to run on a channel that has been hung up\n"
+"  and will not look for hangups.  The external application must exit with\n"
+"  an 'E' command.\n";
 
 /* XXX the parser in gcc 2.95 gets confused if you don't put a space between 'name' and the comma */
 #define ast_chan_log(level, channel, format, ...) ast_log(level, "%s: " format, channel->name , ## __VA_ARGS__)
+
+enum {
+	noanswer = (1 << 0),
+	ignore_hangup = (1 << 1),
+	run_dead = (1 << 2),
+} options_flags;
+
+AST_APP_OPTIONS(app_opts, {
+	AST_APP_OPTION('n', noanswer),
+	AST_APP_OPTION('i', ignore_hangup),
+	AST_APP_OPTION('d', run_dead),
+});
 
 struct playlist_entry {
 	AST_LIST_ENTRY(playlist_entry) list;
@@ -76,6 +94,7 @@
 	int abort_current_sound;
 	int playing_silence;
 	int option_autoclear;
+	int gen_active;
 };
 
 
@@ -88,24 +107,22 @@
 
 static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u, 
 	int eivr_events_fd, int eivr_commands_fd, int eivr_errors_fd, 
-	const char *args);
+	const struct ast_str *args, const struct ast_flags flags);
 
 int eivr_connect_socket(struct ast_channel *chan, const char *host, int port);
 
 static void send_eivr_event(FILE *handle, const char event, const char *data,
 	const struct ast_channel *chan)
 {
-	char tmp[256];
-
-	if (!data) {
-		snprintf(tmp, sizeof(tmp), "%c,%10d", event, (int)time(NULL));
-	} else {
-		snprintf(tmp, sizeof(tmp), "%c,%10d,%s", event, (int)time(NULL), data);
-	}
-
-	fprintf(handle, "%s\n", tmp);
-	if (option_debug)
-		ast_chan_log(LOG_DEBUG, chan, "sent '%s'\n", tmp);
+	struct ast_str *tmp = ast_str_create(12);
+
+	ast_str_append(&tmp, 0, "%c,%10d", event, (int)time(NULL));
+	if (data) {
+		ast_str_append(&tmp, 0, "%s", data);
+	}
+
+	fprintf(handle, "%s\n", tmp->str);
+	ast_debug(1, "sent '%s'\n", tmp->str);
 }
 
 static void *gen_alloc(struct ast_channel *chan, void *params)
@@ -245,7 +262,7 @@
 		variable = strsep(&inbuf, ",");
 		if (variable == NULL) {
 			int outstrlen = strlen(outbuf);
-			if(outstrlen && outbuf[outstrlen - 1] == ',') {
+			if (outstrlen && outbuf[outstrlen - 1] == ',') {
 				outbuf[outstrlen - 1] = 0;
 			}
 			break;
@@ -260,7 +277,7 @@
 		ast_channel_unlock(chan);
 		ast_copy_string(outbuf, newstring->str, outbuflen);
 	}
-};
+}
 
 static void ast_eivr_setvariable(struct ast_channel *chan, char *data)
 {
@@ -273,21 +290,22 @@
 
 	for (j = 1, inbuf = data; ; j++, inbuf = NULL) {
 		variable = strsep(&inbuf, ",");
-		ast_chan_log(LOG_DEBUG, chan, "Setting up a variable: %s\n", variable);
-		if(variable) {
+		ast_debug(1, "Setting up a variable: %s\n", variable);
+		if (variable) {
 			/* variable contains "varname=value" */
 			ast_copy_string(buf, variable, sizeof(buf));
 			value = strchr(buf, '=');
-			if(!value) 
-				value="";
-			else
+			if (!value) {
+				value = "";
+			} else {
 				*value++ = '\0';
+			}
 			pbx_builtin_setvar_helper(chan, buf, value);
-		}
-		else
+		} else {
 			break;
-	}
-};
+		}
+	}
+}
 
 static struct playlist_entry *make_entry(const char *filename)
 {
@@ -303,14 +321,14 @@
 
 static int app_exec(struct ast_channel *chan, void *data)
 {
+	struct ast_flags flags;
+	char *opts[0];
 	struct playlist_entry *entry;
-	int child_stdin[2] = { 0,0 };
-	int child_stdout[2] = { 0,0 };
-	int child_stderr[2] = { 0,0 };
+	int child_stdin[2] = { 0, 0 };
+	int child_stdout[2] = { 0, 0 };
+	int child_stderr[2] = { 0, 0 };
 	int res = -1;
-	int gen_active = 0;
 	int pid;
-	char *buf, *pipe_delim_argbuf, *pdargbuf_ptr;
 
 	char hostname[1024];
 	char *port_str = NULL;
@@ -320,29 +338,87 @@
 	struct ivr_localuser foo = {
 		.playlist = AST_LIST_HEAD_INIT_VALUE,
 		.finishlist = AST_LIST_HEAD_INIT_VALUE,
+		.gen_active = 0,
 	};
 	struct ivr_localuser *u = &foo;
-	AST_DECLARE_APP_ARGS(args,
+
+	char *buf;
+	int j;
+	char *s, **app_args, *e; 
+	struct ast_str *pipe_delim_args = ast_str_create(100);
+
+	AST_DECLARE_APP_ARGS(eivr_args,
 		AST_APP_ARG(cmd)[32];
 	);
+	AST_DECLARE_APP_ARGS(application_args,
+		AST_APP_ARG(cmd)[32];
+	);
 
 	u->abort_current_sound = 0;
 	u->chan = chan;
 
+	buf = ast_strdupa(data);
+	AST_STANDARD_APP_ARGS(eivr_args, buf);
+
+	if ((s = strchr(eivr_args.cmd[0], '('))) {
+		s[0] = ',';
+		if (( e = strrchr(s, ')')) ) {
+			*e = '\0';
+		} else {
+			ast_log(LOG_ERROR, "Parse error, no closing paren?\n");
+		}
+		AST_STANDARD_APP_ARGS(application_args, eivr_args.cmd[0]);
+		app_args = application_args.argv;
+
+		/* Put the application + the arguments in a | delimited list */
+		ast_str_reset(pipe_delim_args);
+		for (j = 0; application_args.cmd[j] != NULL; j++) {
+			ast_str_append(&pipe_delim_args, 0, "%s%s", j == 0 ? "" : "|", application_args.cmd[j]);
+		}
+
+		/* Parse the ExternalIVR() arguments */
+		if (option_debug)
+			ast_debug(1, "Parsing options from: [%s]\n", eivr_args.cmd[1]);
+		ast_app_parse_options(app_opts, &flags, opts, eivr_args.cmd[1]);
+		if (option_debug) {
+			if (ast_test_flag(&flags, noanswer))
+				ast_debug(1, "noanswer is set\n");
+			if (ast_test_flag(&flags, ignore_hangup))
+				ast_debug(1, "ignore_hangup is set\n");
+			if (ast_test_flag(&flags, run_dead))
+				ast_debug(1, "run_dead is set\n");
+		}
+
+	} else {
+		app_args = eivr_args.argv;
+		for (j = 0; eivr_args.cmd[j] != NULL; j++) {
+			ast_str_append(&pipe_delim_args, 0, "%s%s", j == 0 ? "" : "|", eivr_args.cmd[j]);
+		}
+	}
+	
 	if (ast_strlen_zero(data)) {
 		ast_log(LOG_WARNING, "ExternalIVR requires a command to execute\n");
 		return -1;
 	}
 
-	buf = ast_strdupa(data);
-	AST_STANDARD_APP_ARGS(args, buf);
-
-	/* copy args and replace commas with pipes */
-	pipe_delim_argbuf = ast_strdupa(data);
-	while((pdargbuf_ptr = strchr(pipe_delim_argbuf, ',')) != NULL)
-		pdargbuf_ptr[0] = '|';
-
-	if(!strncmp(args.cmd[0], "ivr://", 6)) {
+	if (!(ast_test_flag(&flags, noanswer))) {
+		ast_chan_log(LOG_WARNING, chan, "Answering channel and starting generator\n");
+		if (chan->_state != AST_STATE_UP) {
+			if (ast_test_flag(&flags, run_dead)) {
+				ast_chan_log(LOG_WARNING, chan, "Running ExternalIVR with 'd'ead flag on non-hungup channel isn't supported\n");
+				goto exit;
+			}
+			ast_answer(chan);
+		}
+		if (ast_activate_generator(chan, &gen, u) < 0) {
+			ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
+			goto exit;
+		} else {
+			u->gen_active = 1;
+		}
+	}
+
+	if (!strncmp(app_args[0], "ivr://", 6)) {
 		struct server_args ivr_desc = {
 			.accept_fd = -1,
 			.name = "IVR",
@@ -350,25 +426,16 @@
 		struct ast_hostent hp;
 
 		/*communicate through socket to server*/
-		if (chan->_state != AST_STATE_UP) {
-			ast_answer(chan);
-		}
-		if (ast_activate_generator(chan, &gen, u) < 0) {
-			ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
-			goto exit;
-		} else {
-			gen_active = 1;
-		}
-
-		ast_chan_log(LOG_DEBUG, chan, "Parsing hostname:port for socket connect from \"%s\"\n", args.cmd[0]);           
-		strncpy(hostname, args.cmd[0] + 6, sizeof(hostname));
-		if((port_str = strchr(hostname, ':')) != NULL) {
+		ast_debug(1, "Parsing hostname:port for socket connect from \"%s\"\n", app_args[0]);
+		ast_copy_string(hostname, app_args[0] + 6, sizeof(hostname));
+		if ((port_str = strchr(hostname, ':')) != NULL) {
 			port_str[0] = 0;
 			port_str += 1;
 			port = atoi(port_str);
 		}
-		if(!port)
-			port = 2949;  /*default port, if one is not provided*/
+		if (!port) {
+			port = 2949;  /* default port, if one is not provided */
+		}
 
 		ast_gethostbyname(hostname, &hp);
 		ivr_desc.sin.sin_family = AF_INET;
@@ -378,8 +445,9 @@
 
 		if (!ser) {
 			goto exit;
-		} 
-		res = eivr_comm(chan, u, ser->fd, ser->fd, -1, pipe_delim_argbuf);
+		}
+		res = eivr_comm(chan, u, ser->fd, ser->fd, -1, pipe_delim_args, flags);
+
 	} else {
 	
 		if (pipe(child_stdin)) {
@@ -393,15 +461,6 @@
 		if (pipe(child_stderr)) {
 			ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child errors: %s\n", strerror(errno));
 			goto exit;
-		}
-		if (chan->_state != AST_STATE_UP) {
-			ast_answer(chan);
-		}
-		if (ast_activate_generator(chan, &gen, u) < 0) {
-			ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
-			goto exit;
-		} else {
-			gen_active = 1;
 		}
 	
 		pid = ast_safe_fork(0);
@@ -419,24 +478,23 @@
 			dup2(child_stdout[1], STDOUT_FILENO);
 			dup2(child_stderr[1], STDERR_FILENO);
 			ast_close_fds_above_n(STDERR_FILENO);
-			execv(args.cmd[0], args.cmd);
-			fprintf(stderr, "Failed to execute '%s': %s\n", args.cmd[0], strerror(errno));
+			execv(app_args[0], app_args);
+			fprintf(stderr, "Failed to execute '%s': %s\n", app_args[0], strerror(errno));
 			_exit(1);
 		} else {
 			/* parent process */
-	
 			close(child_stdin[0]);
 			child_stdin[0] = 0;
 			close(child_stdout[1]);
 			child_stdout[1] = 0;
 			close(child_stderr[1]);
 			child_stderr[1] = 0;
-			res = eivr_comm(chan, u, child_stdin[1], child_stdout[0], child_stderr[0], pipe_delim_argbuf);
+			res = eivr_comm(chan, u, child_stdin[1], child_stdout[0], child_stderr[0], pipe_delim_args, flags);
 		}
 	}
 
 	exit:
-	if (gen_active)
+	if (u->gen_active)
 		ast_deactivate_generator(chan);
 
 	if (child_stdin[0])
@@ -456,12 +514,10 @@
 
 	if (child_stderr[1])
 		close(child_stderr[1]);
-
 	if (ser) {
 		fclose(ser->f);
 		ast_tcptls_session_instance_destroy(ser);
 	}
-
 	while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list)))
 		ast_free(entry);
 
@@ -470,7 +526,7 @@
 
 static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u, 
  				int eivr_events_fd, int eivr_commands_fd, int eivr_errors_fd, 
- 				const char *args)
+ 				const struct ast_str *args, const struct ast_flags flags)
 {
 	struct playlist_entry *entry;
 	struct ast_frame *f;
@@ -482,6 +538,7 @@
  	char *command;
  	int res = -1;
 	int test_available_fd = -1;
+	int hangup_info_sent = 0;
   
  	FILE *eivr_commands = NULL;
  	FILE *eivr_errors = NULL;
@@ -506,8 +563,9 @@
  
  	setvbuf(eivr_events, NULL, _IONBF, 0);
  	setvbuf(eivr_commands, NULL, _IONBF, 0);
- 	if(eivr_errors)
+ 	if (eivr_errors) {
 		setvbuf(eivr_errors, NULL, _IONBF, 0);
+	}
 
 	res = 0;
  
@@ -517,11 +575,17 @@
  			res = -1;
  			break;
  		}
- 		if (ast_check_hangup(chan)) {
- 			ast_chan_log(LOG_NOTICE, chan, "Got check_hangup\n");
- 			send_eivr_event(eivr_events, 'H', NULL, chan);
- 			res = -1;
- 			break;
+ 		if (!hangup_info_sent && !(ast_test_flag(&flags, run_dead)) && ast_check_hangup(chan)) {
+			if (ast_test_flag(&flags, ignore_hangup)) {
+				ast_chan_log(LOG_NOTICE, chan, "Got check_hangup, but ignore_hangup set so sending 'I' command\n");
+				send_eivr_event(eivr_events, 'I', "HANGUP", chan);
+				hangup_info_sent = 1;
+			} else {
+ 				ast_chan_log(LOG_NOTICE, chan, "Got check_hangup\n");
+ 				send_eivr_event(eivr_events, 'H', NULL, chan);
+ 				res = -1;
+	 			break;
+			}
  		}
  
  		ready_fd = 0;
@@ -531,7 +595,7 @@
  
  		rchan = ast_waitfor_nandfds(&chan, 1, waitfds, (eivr_errors_fd < 0) ? 1 : 2, &exception, &ready_fd, &ms);
  
- 		if (!AST_LIST_EMPTY(&u->finishlist)) {
+ 		if (chan->_state == AST_STATE_UP && !AST_LIST_EMPTY(&u->finishlist)) {
  			AST_LIST_LOCK(&u->finishlist);
  			while ((entry = AST_LIST_REMOVE_HEAD(&u->finishlist, list))) {
  				send_eivr_event(eivr_events, 'F', entry->filename, chan);
@@ -540,7 +604,7 @@
  			AST_LIST_UNLOCK(&u->finishlist);
  		}
  
- 		if (rchan) {
+ 		if (chan->_state == AST_STATE_UP && !(ast_check_hangup(chan)) && rchan) {
  			/* the channel has something */
  			f = ast_read(chan);
  			if (!f) {
@@ -589,15 +653,37 @@
  			command = ast_strip(input);
   
  			if (option_debug)
- 				ast_chan_log(LOG_DEBUG, chan, "got command '%s'\n", input);
+ 				ast_debug(1, "got command '%s'\n", input);
   
  			if (strlen(input) < 4)
  				continue;
   
 			if (input[0] == 'P') {
- 				send_eivr_event(eivr_events, 'P', args, chan);
- 
+ 				send_eivr_event(eivr_events, 'P', args->str, chan);
+			} else if ( input[0] == 'T' ) {
+				ast_chan_log(LOG_WARNING, chan, "Answering channel if needed and starting generator\n");
+				if (chan->_state != AST_STATE_UP) {
+					if (ast_test_flag(&flags, run_dead)) {
+						ast_chan_log(LOG_WARNING, chan, "Running ExternalIVR with 'd'ead flag on non-hungup channel isn't supported\n");
+						send_eivr_event(eivr_events, 'Z', "ANSWER_FAILURE", chan);
+						continue;
+					}
+					ast_answer(chan);
+				}
+				if (!(u->gen_active)) {
+					if (ast_activate_generator(chan, &gen, u) < 0) {
+						ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
+						send_eivr_event(eivr_events, 'Z', "GENERATOR_FAILURE", chan);
+					} else {
+						u->gen_active = 1;
+					}
+				}
  			} else if (input[0] == 'S') {
+				if (chan->_state != AST_STATE_UP || ast_check_hangup(chan)) {
+					ast_chan_log(LOG_WARNING, chan, "Queue 'S'et called on unanswered channel\n");
+					send_eivr_event(eivr_events, 'Z', NULL, chan);
+					continue;
+				}
  				if (ast_fileexists(&input[2], NULL, u->chan->language) == -1) {
  					ast_chan_log(LOG_WARNING, chan, "Unknown file requested '%s'\n", &input[2]);
  					send_eivr_event(eivr_events, 'Z', NULL, chan);
@@ -617,6 +703,11 @@
  					AST_LIST_INSERT_TAIL(&u->playlist, entry, list);
  				AST_LIST_UNLOCK(&u->playlist);
  			} else if (input[0] == 'A') {
+				if (chan->_state != AST_STATE_UP || ast_check_hangup(chan)) {
+					ast_chan_log(LOG_WARNING, chan, "Queue 'A'ppend called on unanswered channel\n");
+					send_eivr_event(eivr_events, 'Z', NULL, chan);
+					continue;
+				}
  				if (ast_fileexists(&input[2], NULL, u->chan->language) == -1) {
  					ast_chan_log(LOG_WARNING, chan, "Unknown file requested '%s'\n", &input[2]);
  					send_eivr_event(eivr_events, 'Z', NULL, chan);
@@ -657,6 +748,11 @@
  				res = -1;
  				break;
  			} else if (input[0] == 'O') {
+				if (chan->_state != AST_STATE_UP || ast_check_hangup(chan)) {
+					ast_chan_log(LOG_WARNING, chan, "Option called on unanswered channel\n");
+					send_eivr_event(eivr_events, 'Z', NULL, chan);
+					continue;
+				}
  				if (!strcasecmp(&input[2], "autoclear"))
  					u->option_autoclear = 1;
  				else if (!strcasecmp(&input[2], "noautoclear"))

Modified: trunk/doc/externalivr.txt
URL: http://svn.digium.com/view/asterisk/trunk/doc/externalivr.txt?view=diff&rev=117725&r1=117724&r2=117725
==============================================================================
--- trunk/doc/externalivr.txt (original)
+++ trunk/doc/externalivr.txt Thu May 22 00:10:01 2008
@@ -24,7 +24,8 @@
 
 stdin (0) - DTMF and hangup events will be received on this handle
 stdout (1) - Playback and hangup commands can be sent on this handle
-There are no error messages available when using ExternalIVR over TCP.
+There are no error messages available when using ExternalIVR over TCP,
+use the 'L' command as a replacement for this.
 
 The application will also create an audio generator to play audio to
 the channel, and will start playing silence. When your application
@@ -46,6 +47,15 @@
 
 If the child process dies, ExternalIVR() will notice this and hang up
 the channel immediately (and also send a message to the log).
+
+ExternalIVR() Options
+----------------------
+n: 'n'oanswer, don't answer an otherwise unanswered channel.
+i: 'i'gnore_hangup, instead of sending an 'H' event and exiting
+ExternalIVR() upon channel hangup, it instead sends an 'I' event
+and expects the external application to exit the process.
+d: 'd'ead, allows the operation of ExternalIVR() on channels that
+have already been hung up.
 
 DTMF (and other) events
 -----------------------
@@ -71,6 +81,10 @@
 data element will be the dropped file name)
 F: a file has finished playing (the data element will be the file
 name)
+P: a response to the 'P' command (see below)
+G: a response to the 'G' command (see below)
+I: a Inform message, meant to "inform" the client that something
+has occured.  (see Inform Messages below)
 
 The timestamp will be 10 digits long, and will be a decimal
 representation of a standard Unix epoch-based timestamp.
@@ -87,7 +101,11 @@
 H,message
 E,message
 O,option
-V,name=value
+V,name=value[,name=value[,name=value]]
+G,name[,name[,name]]
+L,log_message
+P,TIMESTAMP
+T,TIMESTAMP
 
 The 'S' command checks to see if there is a playable audio file with
 the specified name, and if so, clear's the generator's playlist and
@@ -116,7 +134,25 @@
 	Automatically interrupt and clear the playlist upon reception
 	of DTMF input.
 
-The 'V' command sets the specified channel variable to the specified value.
+The 'T' command will answer and unanswered channel.  If it fails either
+answering the channel or starting the generator it sends a Z response
+of "Z,TIMESTAMP,ANSWER_FAILED" or "Z,TIMESTAMP,GENERATOR_FAILED"
+respectively.
+
+The 'V' command sets the specified channel variable(s) to the specified
+value(s).
+
+The 'G' command gets the specified channel variable(s).  Multiple
+variables are separated by commas.  Response is in name=value format.
+
+The 'P' command gets the parameters passed into ExternalIVR() minus
+the options to ExternalIVR() itself:
+	If ExternalIVR() is executed as:
+		ExternalIVR(/usr/bin/foo(arg1,arg2),n)
+	The response to the 'P' command would be:
+		P,TIMESTAMP,/usr/bin/foo|arg1|arg2
+
+The 'L' command puts a message into the Asterisk log.
 
 Errors
 ------




More information about the asterisk-commits mailing list