aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortilghman <tilghman@f38db490-d61c-443f-a65b-d21fe96a405b>2008-05-22 05:10:01 +0000
committertilghman <tilghman@f38db490-d61c-443f-a65b-d21fe96a405b>2008-05-22 05:10:01 +0000
commit9f974d96fa03801c0cc9fa0dd8accd079ac1358a (patch)
tree88520dee50e5b7da13e396c91a0fb0b3dee68df5
parent549f37623bb6c5f462f63608f453fe21c89292ec (diff)
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 git-svn-id: http://svn.digium.com/svn/asterisk/trunk@117725 f38db490-d61c-443f-a65b-d21fe96a405b
-rw-r--r--CHANGES3
-rw-r--r--apps/app_externalivr.c256
-rw-r--r--doc/externalivr.txt42
3 files changed, 218 insertions, 83 deletions
diff --git a/CHANGES b/CHANGES
index 7c34507dd..9d3c8cd7c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -72,6 +72,9 @@ Application Changes
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
-----------
diff --git a/apps/app_externalivr.c b/apps/app_externalivr.c
index 8dbc53e76..a7369602a 100644
--- a/apps/app_externalivr.c
+++ b/apps/app_externalivr.c
@@ -50,20 +50,38 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
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;
char filename[1];
@@ -76,6 +94,7 @@ struct ivr_localuser {
int abort_current_sound;
int playing_silence;
int option_autoclear;
+ int gen_active;
};
@@ -88,24 +107,22 @@ struct gen_state {
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];
+ struct ast_str *tmp = ast_str_create(12);
- 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);
+ 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);
- if (option_debug)
- ast_chan_log(LOG_DEBUG, chan, "sent '%s'\n", tmp);
+ 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 @@ static void ast_eivr_getvariable(struct ast_channel *chan, char *data, char *out
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 @@ static void ast_eivr_getvariable(struct ast_channel *chan, char *data, char *out
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 @@ static void ast_eivr_setvariable(struct ast_channel *chan, char *data)
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 struct playlist_entry *make_entry(const char *filename)
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,55 +338,104 @@ static int app_exec(struct ast_channel *chan, void *data)
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)) {
- struct server_args ivr_desc = {
- .accept_fd = -1,
- .name = "IVR",
- };
- struct ast_hostent hp;
-
- /*communicate through socket to server*/
+ 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 {
- gen_active = 1;
+ u->gen_active = 1;
}
+ }
+
+ if (!strncmp(app_args[0], "ivr://", 6)) {
+ struct server_args ivr_desc = {
+ .accept_fd = -1,
+ .name = "IVR",
+ };
+ struct ast_hostent hp;
- 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) {
+ /*communicate through socket to server*/
+ 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 @@ static int app_exec(struct ast_channel *chan, void *data)
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)) {
@@ -394,15 +462,6 @@ static int app_exec(struct ast_channel *chan, void *data)
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);
if (pid < 0) {
@@ -419,24 +478,23 @@ static int app_exec(struct ast_channel *chan, void *data)
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 @@ static int app_exec(struct ast_channel *chan, void *data)
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 app_exec(struct ast_channel *chan, void *data)
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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 @@ static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
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"))
diff --git a/doc/externalivr.txt b/doc/externalivr.txt
index b17ed8852..3020f1466 100644
--- a/doc/externalivr.txt
+++ b/doc/externalivr.txt
@@ -24,7 +24,8 @@ connected to the Asterisk process as follows:
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
@@ -47,6 +48,15 @@ or you could cause the channel to become non-responsive.
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 @@ D: a file was dropped from the play list due to interruption (the
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 @@ A,filename
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 @@ ExternalIVR() application. The supported options are:
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
------