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$")
45 #include "asterisk/lock.h"
46 #include "asterisk/file.h"
47 #include "asterisk/logger.h"
48 #include "asterisk/channel.h"
49 #include "asterisk/pbx.h"
50 #include "asterisk/module.h"
51 #include "asterisk/linkedlists.h"
52 #include "asterisk/app.h"
53 #include "asterisk/utils.h"
54 #include "asterisk/options.h"
56 static const char *app
= "ExternalIVR";
58 static const char *synopsis
= "Interfaces with an external IVR application";
60 static const char *descrip
=
61 " ExternalIVR(command[|arg[|arg...]]): Forks a process to run the supplied command,\n"
62 "and starts a generator on the channel. The generator's play list is\n"
63 "controlled by the external application, which can add and clear entries\n"
64 "via simple commands issued over its stdout. The external application\n"
65 "will receive all DTMF events received on the channel, and notification\n"
66 "if the channel is hung up. The application will not be forcibly terminated\n"
67 "when the channel is hung up.\n"
68 "See doc/externalivr.txt for a protocol specification.\n";
70 /* XXX the parser in gcc 2.95 gets confused if you don't put a space between 'name' and the comma */
71 #define ast_chan_log(level, channel, format, ...) ast_log(level, "%s: " format, channel->name , ## __VA_ARGS__)
73 struct playlist_entry
{
74 AST_LIST_ENTRY(playlist_entry
) list
;
78 struct ivr_localuser
{
79 struct ast_channel
*chan
;
80 AST_LIST_HEAD(playlist
, playlist_entry
) playlist
;
81 AST_LIST_HEAD(finishlist
, playlist_entry
) finishlist
;
82 int abort_current_sound
;
89 struct ivr_localuser
*u
;
90 struct ast_filestream
*stream
;
91 struct playlist_entry
*current
;
95 static void send_child_event(FILE *handle
, const char event
, const char *data
,
96 const struct ast_channel
*chan
)
101 snprintf(tmp
, sizeof(tmp
), "%c,%10d", event
, (int)time(NULL
));
103 snprintf(tmp
, sizeof(tmp
), "%c,%10d,%s", event
, (int)time(NULL
), data
);
106 fprintf(handle
, "%s\n", tmp
);
107 ast_chan_log(LOG_DEBUG
, chan
, "sent '%s'\n", tmp
);
110 static void *gen_alloc(struct ast_channel
*chan
, void *params
)
112 struct ivr_localuser
*u
= params
;
113 struct gen_state
*state
;
115 if (!(state
= ast_calloc(1, sizeof(*state
))))
123 static void gen_closestream(struct gen_state
*state
)
128 ast_closestream(state
->stream
);
129 state
->u
->chan
->stream
= NULL
;
130 state
->stream
= NULL
;
133 static void gen_release(struct ast_channel
*chan
, void *data
)
135 struct gen_state
*state
= data
;
137 gen_closestream(state
);
141 /* caller has the playlist locked */
142 static int gen_nextfile(struct gen_state
*state
)
144 struct ivr_localuser
*u
= state
->u
;
145 char *file_to_stream
;
147 u
->abort_current_sound
= 0;
148 u
->playing_silence
= 0;
149 gen_closestream(state
);
151 while (!state
->stream
) {
152 state
->current
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
);
153 if (state
->current
) {
154 file_to_stream
= state
->current
->filename
;
156 file_to_stream
= "silence/10";
157 u
->playing_silence
= 1;
160 if (!(state
->stream
= ast_openstream_full(u
->chan
, file_to_stream
, u
->chan
->language
, 1))) {
161 ast_chan_log(LOG_WARNING
, u
->chan
, "File '%s' could not be opened: %s\n", file_to_stream
, strerror(errno
));
162 if (!u
->playing_silence
) {
170 return (!state
->stream
);
173 static struct ast_frame
*gen_readframe(struct gen_state
*state
)
175 struct ast_frame
*f
= NULL
;
176 struct ivr_localuser
*u
= state
->u
;
178 if (u
->abort_current_sound
||
179 (u
->playing_silence
&& AST_LIST_FIRST(&u
->playlist
))) {
180 gen_closestream(state
);
181 AST_LIST_LOCK(&u
->playlist
);
183 AST_LIST_UNLOCK(&u
->playlist
);
186 if (!(state
->stream
&& (f
= ast_readframe(state
->stream
)))) {
187 if (state
->current
) {
188 AST_LIST_LOCK(&u
->finishlist
);
189 AST_LIST_INSERT_TAIL(&u
->finishlist
, state
->current
, list
);
190 AST_LIST_UNLOCK(&u
->finishlist
);
191 state
->current
= NULL
;
193 if (!gen_nextfile(state
))
194 f
= ast_readframe(state
->stream
);
200 static int gen_generate(struct ast_channel
*chan
, void *data
, int len
, int samples
)
202 struct gen_state
*state
= data
;
203 struct ast_frame
*f
= NULL
;
206 state
->sample_queue
+= samples
;
208 while (state
->sample_queue
> 0) {
209 if (!(f
= gen_readframe(state
)))
212 res
= ast_write(chan
, f
);
215 ast_chan_log(LOG_WARNING
, chan
, "Failed to write frame: %s\n", strerror(errno
));
218 state
->sample_queue
-= f
->samples
;
224 static struct ast_generator gen
=
227 release
: gen_release
,
228 generate
: gen_generate
,
231 static struct playlist_entry
*make_entry(const char *filename
)
233 struct playlist_entry
*entry
;
235 if (!(entry
= ast_calloc(1, sizeof(*entry
) + strlen(filename
) + 10))) /* XXX why 10 ? */
238 strcpy(entry
->filename
, filename
);
243 static int app_exec(struct ast_channel
*chan
, void *data
)
245 struct ast_module_user
*lu
;
246 struct playlist_entry
*entry
;
247 const char *args
= data
;
248 int child_stdin
[2] = { 0,0 };
249 int child_stdout
[2] = { 0,0 };
250 int child_stderr
[2] = { 0,0 };
252 int test_available_fd
= -1;
258 FILE *child_commands
= NULL
;
259 FILE *child_errors
= NULL
;
260 FILE *child_events
= NULL
;
261 struct ivr_localuser foo
= {
262 .playlist
= AST_LIST_HEAD_INIT_VALUE
,
263 .finishlist
= AST_LIST_HEAD_INIT_VALUE
,
265 struct ivr_localuser
*u
= &foo
;
266 sigset_t fullset
, oldset
;
268 lu
= ast_module_user_add(chan
);
270 sigfillset(&fullset
);
271 pthread_sigmask(SIG_BLOCK
, &fullset
, &oldset
);
273 u
->abort_current_sound
= 0;
276 if (ast_strlen_zero(args
)) {
277 ast_log(LOG_WARNING
, "ExternalIVR requires a command to execute\n");
278 ast_module_user_remove(lu
);
282 buf
= ast_strdupa(data
);
284 argc
= ast_app_separate_args(buf
, '|', argv
, sizeof(argv
) / sizeof(argv
[0]));
286 if (pipe(child_stdin
)) {
287 ast_chan_log(LOG_WARNING
, chan
, "Could not create pipe for child input: %s\n", strerror(errno
));
291 if (pipe(child_stdout
)) {
292 ast_chan_log(LOG_WARNING
, chan
, "Could not create pipe for child output: %s\n", strerror(errno
));
296 if (pipe(child_stderr
)) {
297 ast_chan_log(LOG_WARNING
, chan
, "Could not create pipe for child errors: %s\n", strerror(errno
));
301 if (chan
->_state
!= AST_STATE_UP
) {
305 if (ast_activate_generator(chan
, &gen
, u
) < 0) {
306 ast_chan_log(LOG_WARNING
, chan
, "Failed to activate generator\n");
313 ast_log(LOG_WARNING
, "Failed to fork(): %s\n", strerror(errno
));
321 signal(SIGPIPE
, SIG_DFL
);
322 pthread_sigmask(SIG_UNBLOCK
, &fullset
, NULL
);
324 if (ast_opt_high_priority
)
327 dup2(child_stdin
[0], STDIN_FILENO
);
328 dup2(child_stdout
[1], STDOUT_FILENO
);
329 dup2(child_stderr
[1], STDERR_FILENO
);
330 for (i
= STDERR_FILENO
+ 1; i
< 1024; i
++)
332 execv(argv
[0], argv
);
333 fprintf(stderr
, "Failed to execute '%s': %s\n", argv
[0], strerror(errno
));
337 int child_events_fd
= child_stdin
[1];
338 int child_commands_fd
= child_stdout
[0];
339 int child_errors_fd
= child_stderr
[0];
344 int waitfds
[2] = { child_errors_fd
, child_commands_fd
};
345 struct ast_channel
*rchan
;
347 pthread_sigmask(SIG_SETMASK
, &oldset
, NULL
);
349 close(child_stdin
[0]);
351 close(child_stdout
[1]);
353 close(child_stderr
[1]);
356 if (!(child_events
= fdopen(child_events_fd
, "w"))) {
357 ast_chan_log(LOG_WARNING
, chan
, "Could not open stream for child events\n");
361 if (!(child_commands
= fdopen(child_commands_fd
, "r"))) {
362 ast_chan_log(LOG_WARNING
, chan
, "Could not open stream for child commands\n");
366 if (!(child_errors
= fdopen(child_errors_fd
, "r"))) {
367 ast_chan_log(LOG_WARNING
, chan
, "Could not open stream for child errors\n");
371 test_available_fd
= open("/dev/null", O_RDONLY
);
373 setvbuf(child_events
, NULL
, _IONBF
, 0);
374 setvbuf(child_commands
, NULL
, _IONBF
, 0);
375 setvbuf(child_errors
, NULL
, _IONBF
, 0);
380 if (ast_test_flag(chan
, AST_FLAG_ZOMBIE
)) {
381 ast_chan_log(LOG_NOTICE
, chan
, "Is a zombie\n");
386 if (ast_check_hangup(chan
)) {
387 ast_chan_log(LOG_NOTICE
, chan
, "Got check_hangup\n");
388 send_child_event(child_events
, 'H', NULL
, chan
);
398 rchan
= ast_waitfor_nandfds(&chan
, 1, waitfds
, 2, &exception
, &ready_fd
, &ms
);
400 if (!AST_LIST_EMPTY(&u
->finishlist
)) {
401 AST_LIST_LOCK(&u
->finishlist
);
402 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->finishlist
, list
))) {
403 send_child_event(child_events
, 'F', entry
->filename
, chan
);
406 AST_LIST_UNLOCK(&u
->finishlist
);
410 /* the channel has something */
413 ast_chan_log(LOG_NOTICE
, chan
, "Returned no frame\n");
414 send_child_event(child_events
, 'H', NULL
, chan
);
419 if (f
->frametype
== AST_FRAME_DTMF
) {
420 send_child_event(child_events
, f
->subclass
, NULL
, chan
);
421 if (u
->option_autoclear
) {
422 if (!u
->abort_current_sound
&& !u
->playing_silence
)
423 send_child_event(child_events
, 'T', NULL
, chan
);
424 AST_LIST_LOCK(&u
->playlist
);
425 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
))) {
426 send_child_event(child_events
, 'D', entry
->filename
, chan
);
429 if (!u
->playing_silence
)
430 u
->abort_current_sound
= 1;
431 AST_LIST_UNLOCK(&u
->playlist
);
433 } else if ((f
->frametype
== AST_FRAME_CONTROL
) && (f
->subclass
== AST_CONTROL_HANGUP
)) {
434 ast_chan_log(LOG_NOTICE
, chan
, "Got AST_CONTROL_HANGUP\n");
435 send_child_event(child_events
, 'H', NULL
, chan
);
441 } else if (ready_fd
== child_commands_fd
) {
444 if (exception
|| feof(child_commands
)) {
445 ast_chan_log(LOG_WARNING
, chan
, "Child process went away\n");
450 if (!fgets(input
, sizeof(input
), child_commands
))
453 command
= ast_strip(input
);
455 ast_chan_log(LOG_DEBUG
, chan
, "got command '%s'\n", input
);
457 if (strlen(input
) < 4)
460 if (input
[0] == 'S') {
461 if (ast_fileexists(&input
[2], NULL
, u
->chan
->language
) == -1) {
462 ast_chan_log(LOG_WARNING
, chan
, "Unknown file requested '%s'\n", &input
[2]);
463 send_child_event(child_events
, 'Z', NULL
, chan
);
464 strcpy(&input
[2], "exception");
466 if (!u
->abort_current_sound
&& !u
->playing_silence
)
467 send_child_event(child_events
, 'T', NULL
, chan
);
468 AST_LIST_LOCK(&u
->playlist
);
469 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
))) {
470 send_child_event(child_events
, 'D', entry
->filename
, chan
);
473 if (!u
->playing_silence
)
474 u
->abort_current_sound
= 1;
475 entry
= make_entry(&input
[2]);
477 AST_LIST_INSERT_TAIL(&u
->playlist
, entry
, list
);
478 AST_LIST_UNLOCK(&u
->playlist
);
479 } else if (input
[0] == 'A') {
480 if (ast_fileexists(&input
[2], NULL
, u
->chan
->language
) == -1) {
481 ast_chan_log(LOG_WARNING
, chan
, "Unknown file requested '%s'\n", &input
[2]);
482 send_child_event(child_events
, 'Z', NULL
, chan
);
483 strcpy(&input
[2], "exception");
485 entry
= make_entry(&input
[2]);
487 AST_LIST_LOCK(&u
->playlist
);
488 AST_LIST_INSERT_TAIL(&u
->playlist
, entry
, list
);
489 AST_LIST_UNLOCK(&u
->playlist
);
491 } else if (input
[0] == 'H') {
492 ast_chan_log(LOG_NOTICE
, chan
, "Hanging up: %s\n", &input
[2]);
493 send_child_event(child_events
, 'H', NULL
, chan
);
495 } else if (input
[0] == 'O') {
496 if (!strcasecmp(&input
[2], "autoclear"))
497 u
->option_autoclear
= 1;
498 else if (!strcasecmp(&input
[2], "noautoclear"))
499 u
->option_autoclear
= 0;
501 ast_chan_log(LOG_WARNING
, chan
, "Unknown option requested '%s'\n", &input
[2]);
503 } else if (ready_fd
== child_errors_fd
) {
506 if (exception
|| (dup2(child_commands_fd
, test_available_fd
) == -1) || feof(child_errors
)) {
507 ast_chan_log(LOG_WARNING
, chan
, "Child process went away\n");
512 if (fgets(input
, sizeof(input
), child_errors
)) {
513 command
= ast_strip(input
);
514 ast_chan_log(LOG_NOTICE
, chan
, "stderr: %s\n", command
);
516 } else if ((ready_fd
< 0) && ms
) {
517 if (errno
== 0 || errno
== EINTR
)
520 ast_chan_log(LOG_WARNING
, chan
, "Wait failed (%s)\n", strerror(errno
));
528 ast_deactivate_generator(chan
);
531 fclose(child_events
);
534 fclose(child_commands
);
537 fclose(child_errors
);
539 if (test_available_fd
> -1) {
540 close(test_available_fd
);
544 close(child_stdin
[0]);
547 close(child_stdin
[1]);
550 close(child_stdout
[0]);
553 close(child_stdout
[1]);
556 close(child_stderr
[0]);
559 close(child_stderr
[1]);
561 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
)))
564 ast_module_user_remove(lu
);
569 static int unload_module(void)
573 res
= ast_unregister_application(app
);
575 ast_module_user_hangup_all();
580 static int load_module(void)
582 return ast_register_application(app
, app_exec
, synopsis
, descrip
);
585 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY
, "External IVR Interface Application");