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 an 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 };
257 FILE *child_commands
= NULL
;
258 FILE *child_errors
= NULL
;
259 FILE *child_events
= NULL
;
260 struct ivr_localuser foo
= {
261 .playlist
= AST_LIST_HEAD_INIT_VALUE
,
262 .finishlist
= AST_LIST_HEAD_INIT_VALUE
,
264 struct ivr_localuser
*u
= &foo
;
265 sigset_t fullset
, oldset
;
267 lu
= ast_module_user_add(chan
);
269 sigfillset(&fullset
);
270 pthread_sigmask(SIG_BLOCK
, &fullset
, &oldset
);
272 u
->abort_current_sound
= 0;
275 if (ast_strlen_zero(args
)) {
276 ast_log(LOG_WARNING
, "ExternalIVR requires a command to execute\n");
277 ast_module_user_remove(lu
);
281 buf
= ast_strdupa(data
);
283 argc
= ast_app_separate_args(buf
, '|', argv
, sizeof(argv
) / sizeof(argv
[0]));
285 if (pipe(child_stdin
)) {
286 ast_chan_log(LOG_WARNING
, chan
, "Could not create pipe for child input: %s\n", strerror(errno
));
290 if (pipe(child_stdout
)) {
291 ast_chan_log(LOG_WARNING
, chan
, "Could not create pipe for child output: %s\n", strerror(errno
));
295 if (pipe(child_stderr
)) {
296 ast_chan_log(LOG_WARNING
, chan
, "Could not create pipe for child errors: %s\n", strerror(errno
));
300 if (chan
->_state
!= AST_STATE_UP
) {
304 if (ast_activate_generator(chan
, &gen
, u
) < 0) {
305 ast_chan_log(LOG_WARNING
, chan
, "Failed to activate generator\n");
312 ast_log(LOG_WARNING
, "Failed to fork(): %s\n", strerror(errno
));
320 signal(SIGPIPE
, SIG_DFL
);
321 pthread_sigmask(SIG_UNBLOCK
, &fullset
, NULL
);
323 if (ast_opt_high_priority
)
326 dup2(child_stdin
[0], STDIN_FILENO
);
327 dup2(child_stdout
[1], STDOUT_FILENO
);
328 dup2(child_stderr
[1], STDERR_FILENO
);
329 for (i
= STDERR_FILENO
+ 1; i
< 1024; i
++)
331 execv(argv
[0], argv
);
332 fprintf(stderr
, "Failed to execute '%s': %s\n", argv
[0], strerror(errno
));
336 int child_events_fd
= child_stdin
[1];
337 int child_commands_fd
= child_stdout
[0];
338 int child_errors_fd
= child_stderr
[0];
343 int waitfds
[2] = { child_errors_fd
, child_commands_fd
};
344 struct ast_channel
*rchan
;
346 pthread_sigmask(SIG_SETMASK
, &oldset
, NULL
);
348 close(child_stdin
[0]);
350 close(child_stdout
[1]);
352 close(child_stderr
[1]);
355 if (!(child_events
= fdopen(child_events_fd
, "w"))) {
356 ast_chan_log(LOG_WARNING
, chan
, "Could not open stream for child events\n");
360 if (!(child_commands
= fdopen(child_commands_fd
, "r"))) {
361 ast_chan_log(LOG_WARNING
, chan
, "Could not open stream for child commands\n");
365 if (!(child_errors
= fdopen(child_errors_fd
, "r"))) {
366 ast_chan_log(LOG_WARNING
, chan
, "Could not open stream for child errors\n");
370 setvbuf(child_events
, NULL
, _IONBF
, 0);
371 setvbuf(child_commands
, NULL
, _IONBF
, 0);
372 setvbuf(child_errors
, NULL
, _IONBF
, 0);
377 if (ast_test_flag(chan
, AST_FLAG_ZOMBIE
)) {
378 ast_chan_log(LOG_NOTICE
, chan
, "Is a zombie\n");
383 if (ast_check_hangup(chan
)) {
384 ast_chan_log(LOG_NOTICE
, chan
, "Got check_hangup\n");
385 send_child_event(child_events
, 'H', NULL
, chan
);
395 rchan
= ast_waitfor_nandfds(&chan
, 1, waitfds
, 2, &exception
, &ready_fd
, &ms
);
397 if (!AST_LIST_EMPTY(&u
->finishlist
)) {
398 AST_LIST_LOCK(&u
->finishlist
);
399 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->finishlist
, list
))) {
400 send_child_event(child_events
, 'F', entry
->filename
, chan
);
403 AST_LIST_UNLOCK(&u
->finishlist
);
407 /* the channel has something */
410 ast_chan_log(LOG_NOTICE
, chan
, "Returned no frame\n");
411 send_child_event(child_events
, 'H', NULL
, chan
);
416 if (f
->frametype
== AST_FRAME_DTMF
) {
417 send_child_event(child_events
, f
->subclass
, NULL
, chan
);
418 if (u
->option_autoclear
) {
419 if (!u
->abort_current_sound
&& !u
->playing_silence
)
420 send_child_event(child_events
, 'T', NULL
, chan
);
421 AST_LIST_LOCK(&u
->playlist
);
422 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
))) {
423 send_child_event(child_events
, 'D', entry
->filename
, chan
);
426 if (!u
->playing_silence
)
427 u
->abort_current_sound
= 1;
428 AST_LIST_UNLOCK(&u
->playlist
);
430 } else if ((f
->frametype
== AST_FRAME_CONTROL
) && (f
->subclass
== AST_CONTROL_HANGUP
)) {
431 ast_chan_log(LOG_NOTICE
, chan
, "Got AST_CONTROL_HANGUP\n");
432 send_child_event(child_events
, 'H', NULL
, chan
);
438 } else if (ready_fd
== child_commands_fd
) {
441 if (exception
|| feof(child_commands
)) {
442 ast_chan_log(LOG_WARNING
, chan
, "Child process went away\n");
447 if (!fgets(input
, sizeof(input
), child_commands
))
450 command
= ast_strip(input
);
452 ast_chan_log(LOG_DEBUG
, chan
, "got command '%s'\n", input
);
454 if (strlen(input
) < 4)
457 if (input
[0] == 'S') {
458 if (ast_fileexists(&input
[2], NULL
, u
->chan
->language
) == -1) {
459 ast_chan_log(LOG_WARNING
, chan
, "Unknown file requested '%s'\n", &input
[2]);
460 send_child_event(child_events
, 'Z', NULL
, chan
);
461 strcpy(&input
[2], "exception");
463 if (!u
->abort_current_sound
&& !u
->playing_silence
)
464 send_child_event(child_events
, 'T', NULL
, chan
);
465 AST_LIST_LOCK(&u
->playlist
);
466 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
))) {
467 send_child_event(child_events
, 'D', entry
->filename
, chan
);
470 if (!u
->playing_silence
)
471 u
->abort_current_sound
= 1;
472 entry
= make_entry(&input
[2]);
474 AST_LIST_INSERT_TAIL(&u
->playlist
, entry
, list
);
475 AST_LIST_UNLOCK(&u
->playlist
);
476 } else if (input
[0] == 'A') {
477 if (ast_fileexists(&input
[2], NULL
, u
->chan
->language
) == -1) {
478 ast_chan_log(LOG_WARNING
, chan
, "Unknown file requested '%s'\n", &input
[2]);
479 send_child_event(child_events
, 'Z', NULL
, chan
);
480 strcpy(&input
[2], "exception");
482 entry
= make_entry(&input
[2]);
484 AST_LIST_LOCK(&u
->playlist
);
485 AST_LIST_INSERT_TAIL(&u
->playlist
, entry
, list
);
486 AST_LIST_UNLOCK(&u
->playlist
);
488 } else if (input
[0] == 'H') {
489 ast_chan_log(LOG_NOTICE
, chan
, "Hanging up: %s\n", &input
[2]);
490 send_child_event(child_events
, 'H', NULL
, chan
);
492 } else if (input
[0] == 'O') {
493 if (!strcasecmp(&input
[2], "autoclear"))
494 u
->option_autoclear
= 1;
495 else if (!strcasecmp(&input
[2], "noautoclear"))
496 u
->option_autoclear
= 0;
498 ast_chan_log(LOG_WARNING
, chan
, "Unknown option requested '%s'\n", &input
[2]);
500 } else if (ready_fd
== child_errors_fd
) {
503 if (exception
|| feof(child_errors
)) {
504 ast_chan_log(LOG_WARNING
, chan
, "Child process went away\n");
509 if (fgets(input
, sizeof(input
), child_errors
)) {
510 command
= ast_strip(input
);
511 ast_chan_log(LOG_NOTICE
, chan
, "stderr: %s\n", command
);
513 } else if ((ready_fd
< 0) && ms
) {
514 if (errno
== 0 || errno
== EINTR
)
517 ast_chan_log(LOG_WARNING
, chan
, "Wait failed (%s)\n", strerror(errno
));
525 ast_deactivate_generator(chan
);
528 fclose(child_events
);
531 fclose(child_commands
);
534 fclose(child_errors
);
537 close(child_stdin
[0]);
540 close(child_stdin
[1]);
543 close(child_stdout
[0]);
546 close(child_stdout
[1]);
549 close(child_stderr
[0]);
552 close(child_stderr
[1]);
554 while ((entry
= AST_LIST_REMOVE_HEAD(&u
->playlist
, list
)))
557 ast_module_user_remove(lu
);
562 static int unload_module(void)
566 res
= ast_unregister_application(app
);
568 ast_module_user_hangup_all();
573 static int load_module(void)
575 return ast_register_application(app
, app_exec
, synopsis
, descrip
);
578 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY
, "External IVR Interface Application");