2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 1999 - 2005, Digium, Inc.
6 * Kevin P. Fleming <kpfleming@digium.com>
8 * Portions taken from the file-based music-on-hold work
9 * created by Anthony Minessale II in res_musiconhold.c
11 * See http://www.asterisk.org for more information about
12 * the Asterisk project. Please do not directly contact
13 * any of the maintainers of this project for assistance;
14 * the project provides a web site, mailing lists and IRC
15 * channels for your use.
17 * This program is free software, distributed under the terms of
18 * the GNU General Public License Version 2. See the LICENSE file
19 * at the top of the source tree.
24 * \brief External IVR application interface
26 * \author Kevin P. Fleming <kpfleming@digium.com>
28 * \note Portions taken from the file-based music-on-hold work
29 * created by Anthony Minessale II in res_musiconhold.c
31 * \ingroup applications
36 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
40 #include "asterisk/lock.h"
41 #include "asterisk/file.h"
42 #include "asterisk/channel.h"
43 #include "asterisk/pbx.h"
44 #include "asterisk/module.h"
45 #include "asterisk/linkedlists.h"
46 #include "asterisk/app.h"
47 #include "asterisk/utils.h"
49 static const char *app = "ExternalIVR";
51 static const char *synopsis = "Interfaces with an external IVR application";
53 static const char *descrip =
54 " ExternalIVR(command[,arg[,arg...]]): Forks a process to run the supplied command,\n"
55 "and starts a generator on the channel. The generator's play list is\n"
56 "controlled by the external application, which can add and clear entries\n"
57 "via simple commands issued over its stdout. The external application\n"
58 "will receive all DTMF events received on the channel, and notification\n"
59 "if the channel is hung up. The application will not be forcibly terminated\n"
60 "when the channel is hung up.\n"
61 "See doc/externalivr.txt for a protocol specification.\n";
63 /* XXX the parser in gcc 2.95 gets confused if you don't put a space between 'name' and the comma */
64 #define ast_chan_log(level, channel, format, ...) ast_log(level, "%s: " format, channel->name , ## __VA_ARGS__)
66 struct playlist_entry {
67 AST_LIST_ENTRY(playlist_entry) list;
71 struct ivr_localuser {
72 struct ast_channel *chan;
73 AST_LIST_HEAD(playlist, playlist_entry) playlist;
74 AST_LIST_HEAD(finishlist, playlist_entry) finishlist;
75 int abort_current_sound;
82 struct ivr_localuser *u;
83 struct ast_filestream *stream;
84 struct playlist_entry *current;
88 static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
89 int eivr_events_fd, int eivr_commands_fd, int eivr_errors_fd,
92 static void send_eivr_event(FILE *handle, const char event, const char *data,
93 const struct ast_channel *chan)
98 snprintf(tmp, sizeof(tmp), "%c,%10d", event, (int)time(NULL));
100 snprintf(tmp, sizeof(tmp), "%c,%10d,%s", event, (int)time(NULL), data);
103 fprintf(handle, "%s\n", tmp);
105 ast_chan_log(LOG_DEBUG, chan, "sent '%s'\n", tmp);
108 static void *gen_alloc(struct ast_channel *chan, void *params)
110 struct ivr_localuser *u = params;
111 struct gen_state *state;
113 if (!(state = ast_calloc(1, sizeof(*state))))
121 static void gen_closestream(struct gen_state *state)
126 ast_closestream(state->stream);
127 state->u->chan->stream = NULL;
128 state->stream = NULL;
131 static void gen_release(struct ast_channel *chan, void *data)
133 struct gen_state *state = data;
135 gen_closestream(state);
139 /* caller has the playlist locked */
140 static int gen_nextfile(struct gen_state *state)
142 struct ivr_localuser *u = state->u;
143 char *file_to_stream;
145 u->abort_current_sound = 0;
146 u->playing_silence = 0;
147 gen_closestream(state);
149 while (!state->stream) {
150 state->current = AST_LIST_REMOVE_HEAD(&u->playlist, list);
151 if (state->current) {
152 file_to_stream = state->current->filename;
154 file_to_stream = "silence/10";
155 u->playing_silence = 1;
158 if (!(state->stream = ast_openstream_full(u->chan, file_to_stream, u->chan->language, 1))) {
159 ast_chan_log(LOG_WARNING, u->chan, "File '%s' could not be opened: %s\n", file_to_stream, strerror(errno));
160 if (!u->playing_silence) {
168 return (!state->stream);
171 static struct ast_frame *gen_readframe(struct gen_state *state)
173 struct ast_frame *f = NULL;
174 struct ivr_localuser *u = state->u;
176 if (u->abort_current_sound ||
177 (u->playing_silence && AST_LIST_FIRST(&u->playlist))) {
178 gen_closestream(state);
179 AST_LIST_LOCK(&u->playlist);
181 AST_LIST_UNLOCK(&u->playlist);
184 if (!(state->stream && (f = ast_readframe(state->stream)))) {
185 if (state->current) {
186 AST_LIST_LOCK(&u->finishlist);
187 AST_LIST_INSERT_TAIL(&u->finishlist, state->current, list);
188 AST_LIST_UNLOCK(&u->finishlist);
189 state->current = NULL;
191 if (!gen_nextfile(state))
192 f = ast_readframe(state->stream);
198 static int gen_generate(struct ast_channel *chan, void *data, int len, int samples)
200 struct gen_state *state = data;
201 struct ast_frame *f = NULL;
204 state->sample_queue += samples;
206 while (state->sample_queue > 0) {
207 if (!(f = gen_readframe(state)))
210 res = ast_write(chan, f);
213 ast_chan_log(LOG_WARNING, chan, "Failed to write frame: %s\n", strerror(errno));
216 state->sample_queue -= f->samples;
222 static struct ast_generator gen =
225 release: gen_release,
226 generate: gen_generate,
229 static void ast_eivr_getvariable(struct ast_channel *chan, char *data, char *outbuf, int outbuflen)
231 /* original input data: "G,var1,var2," */
232 /* data passed as "data": "var1,var2" */
233 char *inbuf, *variable;
241 for (j = 1, inbuf = data; ; j++, inbuf = NULL) {
242 variable = strtok_r(inbuf, ",", &saveptr);
243 if (variable == NULL) {
244 int outstrlen = strlen(outbuf);
245 if(outstrlen && outbuf[outstrlen - 1] == ',') {
246 outbuf[outstrlen - 1] = 0;
251 value = pbx_builtin_getvar_helper(chan, variable);
254 strncat(outbuf,variable,outbuflen);
255 strncat(outbuf,"=",outbuflen);
256 strncat(outbuf,value,outbuflen);
257 strncat(outbuf,",",outbuflen);
261 static void ast_eivr_setvariable(struct ast_channel *chan, char *data)
266 char *inbuf, *variable;
271 for(j=1, inbuf=data; ; j++, inbuf=NULL) {
272 variable = strtok_r(inbuf, ",", &saveptr);
273 ast_chan_log(LOG_DEBUG, chan, "Setting up a variable: %s\n", variable);
275 /* variable contains "varname=value" */
276 strncpy(buf, variable, sizeof(buf));
277 value = strchr(buf, '=');
284 pbx_builtin_setvar_helper(chan, buf, value);
292 static struct playlist_entry *make_entry(const char *filename)
294 struct playlist_entry *entry;
296 if (!(entry = ast_calloc(1, sizeof(*entry) + strlen(filename) + 10))) /* XXX why 10 ? */
299 strcpy(entry->filename, filename);
304 static int app_exec(struct ast_channel *chan, void *data)
306 struct playlist_entry *entry;
307 int child_stdin[2] = { 0,0 };
308 int child_stdout[2] = { 0,0 };
309 int child_stderr[2] = { 0,0 };
313 char *buf, *pipe_delim_argbuf, *pdargbuf_ptr;
314 struct ivr_localuser foo = {
315 .playlist = AST_LIST_HEAD_INIT_VALUE,
316 .finishlist = AST_LIST_HEAD_INIT_VALUE,
318 struct ivr_localuser *u = &foo;
319 sigset_t fullset, oldset;
320 AST_DECLARE_APP_ARGS(args,
321 AST_APP_ARG(cmd)[32];
324 sigfillset(&fullset);
325 pthread_sigmask(SIG_BLOCK, &fullset, &oldset);
327 u->abort_current_sound = 0;
330 if (ast_strlen_zero(data)) {
331 ast_log(LOG_WARNING, "ExternalIVR requires a command to execute\n");
335 buf = ast_strdupa(data);
336 AST_STANDARD_APP_ARGS(args, buf);
338 /* copy args and replace commas with pipes */
339 pipe_delim_argbuf = ast_strdupa(data);
340 while((pdargbuf_ptr = strchr(pipe_delim_argbuf, ',')) != NULL)
341 pdargbuf_ptr[0] = '|';
343 if (pipe(child_stdin)) {
344 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child input: %s\n", strerror(errno));
347 if (pipe(child_stdout)) {
348 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child output: %s\n", strerror(errno));
351 if (pipe(child_stderr)) {
352 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child errors: %s\n", strerror(errno));
355 if (chan->_state != AST_STATE_UP) {
358 if (ast_activate_generator(chan, &gen, u) < 0) {
359 ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
366 ast_log(LOG_WARNING, "Failed to fork(): %s\n", strerror(errno));
374 signal(SIGPIPE, SIG_DFL);
375 pthread_sigmask(SIG_UNBLOCK, &fullset, NULL);
377 if (ast_opt_high_priority)
380 dup2(child_stdin[0], STDIN_FILENO);
381 dup2(child_stdout[1], STDOUT_FILENO);
382 dup2(child_stderr[1], STDERR_FILENO);
383 for (i = STDERR_FILENO + 1; i < 1024; i++)
385 execv(args.cmd[0], args.cmd);
386 fprintf(stderr, "Failed to execute '%s': %s\n", args.cmd[0], strerror(errno));
391 close(child_stdin[0]);
393 close(child_stdout[1]);
395 close(child_stderr[1]);
397 res = eivr_comm(chan, u, child_stdin[1], child_stdout[0], child_stderr[0], pipe_delim_argbuf);
401 ast_deactivate_generator(chan);
404 close(child_stdin[0]);
407 close(child_stdin[1]);
410 close(child_stdout[0]);
413 close(child_stdout[1]);
416 close(child_stderr[0]);
419 close(child_stderr[1]);
421 while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list)))
428 static int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u,
429 int eivr_events_fd, int eivr_commands_fd, int eivr_errors_fd,
432 struct playlist_entry *entry;
437 int waitfds[2] = { eivr_commands_fd, eivr_errors_fd };
438 struct ast_channel *rchan;
442 FILE *eivr_commands = NULL;
443 FILE *eivr_errors = NULL;
444 FILE *eivr_events = NULL;
446 if (!(eivr_events = fdopen(eivr_events_fd, "w"))) {
447 ast_chan_log(LOG_WARNING, chan, "Could not open stream to send events\n");
450 if (!(eivr_commands = fdopen(eivr_commands_fd, "r"))) {
451 ast_chan_log(LOG_WARNING, chan, "Could not open stream to receive commands\n");
454 if(eivr_errors_fd) { /*if opening a socket connection, error stream will not be used*/
455 if (!(eivr_errors = fdopen(eivr_errors_fd, "r"))) {
456 ast_chan_log(LOG_WARNING, chan, "Could not open stream to receive errors\n");
461 setvbuf(eivr_events, NULL, _IONBF, 0);
462 setvbuf(eivr_commands, NULL, _IONBF, 0);
464 setvbuf(eivr_errors, NULL, _IONBF, 0);
469 if (ast_test_flag(chan, AST_FLAG_ZOMBIE)) {
470 ast_chan_log(LOG_NOTICE, chan, "Is a zombie\n");
474 if (ast_check_hangup(chan)) {
475 ast_chan_log(LOG_NOTICE, chan, "Got check_hangup\n");
476 send_eivr_event(eivr_events, 'H', NULL, chan);
486 rchan = ast_waitfor_nandfds(&chan, 1, waitfds, 2, &exception, &ready_fd, &ms);
488 if (!AST_LIST_EMPTY(&u->finishlist)) {
489 AST_LIST_LOCK(&u->finishlist);
490 while ((entry = AST_LIST_REMOVE_HEAD(&u->finishlist, list))) {
491 send_eivr_event(eivr_events, 'F', entry->filename, chan);
494 AST_LIST_UNLOCK(&u->finishlist);
498 /* the channel has something */
501 ast_chan_log(LOG_NOTICE, chan, "Returned no frame\n");
502 send_eivr_event(eivr_events, 'H', NULL, chan);
506 if (f->frametype == AST_FRAME_DTMF) {
507 send_eivr_event(eivr_events, f->subclass, NULL, chan);
508 if (u->option_autoclear) {
509 if (!u->abort_current_sound && !u->playing_silence)
510 send_eivr_event(eivr_events, 'T', NULL, chan);
511 AST_LIST_LOCK(&u->playlist);
512 while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list))) {
513 send_eivr_event(eivr_events, 'D', entry->filename, chan);
516 if (!u->playing_silence)
517 u->abort_current_sound = 1;
518 AST_LIST_UNLOCK(&u->playlist);
520 } else if ((f->frametype == AST_FRAME_CONTROL) && (f->subclass == AST_CONTROL_HANGUP)) {
521 ast_chan_log(LOG_NOTICE, chan, "Got AST_CONTROL_HANGUP\n");
522 send_eivr_event(eivr_events, 'H', NULL, chan);
528 } else if (ready_fd == eivr_commands_fd) {
531 if (exception || feof(eivr_commands)) {
532 ast_chan_log(LOG_WARNING, chan, "Child process went away\n");
537 if (!fgets(input, sizeof(input), eivr_commands))
540 command = ast_strip(input);
543 ast_chan_log(LOG_DEBUG, chan, "got command '%s'\n", input);
545 if (strlen(input) < 4)
548 if (input[0] == 'P') {
549 send_eivr_event(eivr_events, 'P', args, chan);
551 } else if (input[0] == 'S') {
552 if (ast_fileexists(&input[2], NULL, u->chan->language) == -1) {
553 ast_chan_log(LOG_WARNING, chan, "Unknown file requested '%s'\n", &input[2]);
554 send_eivr_event(eivr_events, 'Z', NULL, chan);
555 strcpy(&input[2], "exception");
557 if (!u->abort_current_sound && !u->playing_silence)
558 send_eivr_event(eivr_events, 'T', NULL, chan);
559 AST_LIST_LOCK(&u->playlist);
560 while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list))) {
561 send_eivr_event(eivr_events, 'D', entry->filename, chan);
564 if (!u->playing_silence)
565 u->abort_current_sound = 1;
566 entry = make_entry(&input[2]);
568 AST_LIST_INSERT_TAIL(&u->playlist, entry, list);
569 AST_LIST_UNLOCK(&u->playlist);
570 } else if (input[0] == 'A') {
571 if (ast_fileexists(&input[2], NULL, u->chan->language) == -1) {
572 ast_chan_log(LOG_WARNING, chan, "Unknown file requested '%s'\n", &input[2]);
573 send_eivr_event(eivr_events, 'Z', NULL, chan);
574 strcpy(&input[2], "exception");
576 entry = make_entry(&input[2]);
578 AST_LIST_LOCK(&u->playlist);
579 AST_LIST_INSERT_TAIL(&u->playlist, entry, list);
580 AST_LIST_UNLOCK(&u->playlist);
582 } else if (input[0] == 'G') {
583 /* A get variable message: "G,variable1,variable2,..." */
586 ast_chan_log(LOG_NOTICE, chan, "Getting a Variable out of the channel: %s\n", &input[2]);
587 ast_eivr_getvariable(chan, &input[2], response, sizeof(response));
588 send_eivr_event(eivr_events, 'G', response, chan);
589 } else if (input[0] == 'V') {
590 /* A set variable message: "V,variablename=foo" */
591 ast_chan_log(LOG_NOTICE, chan, "Setting a Variable up: %s\n", &input[2]);
592 ast_eivr_setvariable(chan, &input[2]);
593 } else if (input[0] == 'L') {
594 ast_chan_log(LOG_NOTICE, chan, "Log message from EIVR: %s\n", &input[2]);
595 } else if (input[0] == 'X') {
596 ast_chan_log(LOG_NOTICE, chan, "Exiting ExternalIVR: %s\n", &input[2]);
597 /*! \todo add deprecation debug message for X command here */
600 } else if (input[0] == 'E') {
601 ast_chan_log(LOG_NOTICE, chan, "Exiting: %s\n", &input[2]);
602 send_eivr_event(eivr_events, 'E', NULL, chan);
605 } else if (input[0] == 'H') {
606 ast_chan_log(LOG_NOTICE, chan, "Hanging up: %s\n", &input[2]);
607 send_eivr_event(eivr_events, 'H', NULL, chan);
610 } else if (input[0] == 'O') {
611 if (!strcasecmp(&input[2], "autoclear"))
612 u->option_autoclear = 1;
613 else if (!strcasecmp(&input[2], "noautoclear"))
614 u->option_autoclear = 0;
616 ast_chan_log(LOG_WARNING, chan, "Unknown option requested '%s'\n", &input[2]);
618 } else if (eivr_errors_fd && ready_fd == eivr_errors_fd) {
621 if (exception || feof(eivr_errors)) {
622 ast_chan_log(LOG_WARNING, chan, "Child process went away\n");
626 if (fgets(input, sizeof(input), eivr_errors)) {
627 command = ast_strip(input);
628 ast_chan_log(LOG_NOTICE, chan, "stderr: %s\n", command);
630 } else if ((ready_fd < 0) && ms) {
631 if (errno == 0 || errno == EINTR)
634 ast_chan_log(LOG_WARNING, chan, "Wait failed (%s)\n", strerror(errno));
646 fclose(eivr_commands);
655 static int unload_module(void)
657 return ast_unregister_application(app);
660 static int load_module(void)
662 return ast_register_application(app, app_exec, synopsis, descrip);
665 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "External IVR Interface Application");