8092fd92ec2e688fd04d28508ff3d4785de7644a
[asterisk/asterisk.git] / apps / app_externalivr.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Kevin P. Fleming <kpfleming@digium.com>
7  *
8  * Portions taken from the file-based music-on-hold work
9  * created by Anthony Minessale II in res_musiconhold.c
10  *
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.
16  *
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.
20  */
21
22 /*! \file
23  *
24  * \brief External IVR application interface
25  *
26  * \author Kevin P. Fleming <kpfleming@digium.com>
27  *
28  * \note Portions taken from the file-based music-on-hold work
29  * created by Anthony Minessale II in res_musiconhold.c
30  *
31  * \ingroup applications
32  */
33
34 #include "asterisk.h"
35
36 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
37
38 #include <signal.h>
39
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"
48
49 static const char *app = "ExternalIVR";
50
51 static const char *synopsis = "Interfaces with an external IVR application";
52
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";
62
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__)
65
66 struct playlist_entry {
67         AST_LIST_ENTRY(playlist_entry) list;
68         char filename[1];
69 };
70
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;
76         int playing_silence;
77         int option_autoclear;
78 };
79
80
81 struct gen_state {
82         struct ivr_localuser *u;
83         struct ast_filestream *stream;
84         struct playlist_entry *current;
85         int sample_queue;
86 };
87
88 static void send_child_event(FILE *handle, const char event, const char *data,
89         const struct ast_channel *chan)
90 {
91         char tmp[256];
92
93         if (!data) {
94                 snprintf(tmp, sizeof(tmp), "%c,%10d", event, (int)time(NULL));
95         } else {
96                 snprintf(tmp, sizeof(tmp), "%c,%10d,%s", event, (int)time(NULL), data);
97         }
98
99         fprintf(handle, "%s\n", tmp);
100         if (option_debug)
101                 ast_chan_log(LOG_DEBUG, chan, "sent '%s'\n", tmp);
102 }
103
104 static void *gen_alloc(struct ast_channel *chan, void *params)
105 {
106         struct ivr_localuser *u = params;
107         struct gen_state *state;
108
109         if (!(state = ast_calloc(1, sizeof(*state))))
110                 return NULL;
111
112         state->u = u;
113
114         return state;
115 }
116
117 static void gen_closestream(struct gen_state *state)
118 {
119         if (!state->stream)
120                 return;
121
122         ast_closestream(state->stream);
123         state->u->chan->stream = NULL;
124         state->stream = NULL;
125 }
126
127 static void gen_release(struct ast_channel *chan, void *data)
128 {
129         struct gen_state *state = data;
130
131         gen_closestream(state);
132         ast_free(data);
133 }
134
135 /* caller has the playlist locked */
136 static int gen_nextfile(struct gen_state *state)
137 {
138         struct ivr_localuser *u = state->u;
139         char *file_to_stream;
140
141         u->abort_current_sound = 0;
142         u->playing_silence = 0;
143         gen_closestream(state);
144
145         while (!state->stream) {
146                 state->current = AST_LIST_REMOVE_HEAD(&u->playlist, list);
147                 if (state->current) {
148                         file_to_stream = state->current->filename;
149                 } else {
150                         file_to_stream = "silence/10";
151                         u->playing_silence = 1;
152                 }
153
154                 if (!(state->stream = ast_openstream_full(u->chan, file_to_stream, u->chan->language, 1))) {
155                         ast_chan_log(LOG_WARNING, u->chan, "File '%s' could not be opened: %s\n", file_to_stream, strerror(errno));
156                         if (!u->playing_silence) {
157                                 continue;
158                         } else {
159                                 break;
160                         }
161                 }
162         }
163
164         return (!state->stream);
165 }
166
167 static struct ast_frame *gen_readframe(struct gen_state *state)
168 {
169         struct ast_frame *f = NULL;
170         struct ivr_localuser *u = state->u;
171
172         if (u->abort_current_sound ||
173                 (u->playing_silence && AST_LIST_FIRST(&u->playlist))) {
174                 gen_closestream(state);
175                 AST_LIST_LOCK(&u->playlist);
176                 gen_nextfile(state);
177                 AST_LIST_UNLOCK(&u->playlist);
178         }
179
180         if (!(state->stream && (f = ast_readframe(state->stream)))) {
181                 if (state->current) {
182                         AST_LIST_LOCK(&u->finishlist);
183                         AST_LIST_INSERT_TAIL(&u->finishlist, state->current, list);
184                         AST_LIST_UNLOCK(&u->finishlist);
185                         state->current = NULL;
186                 }
187                 if (!gen_nextfile(state))
188                         f = ast_readframe(state->stream);
189         }
190
191         return f;
192 }
193
194 static int gen_generate(struct ast_channel *chan, void *data, int len, int samples)
195 {
196         struct gen_state *state = data;
197         struct ast_frame *f = NULL;
198         int res = 0;
199
200         state->sample_queue += samples;
201
202         while (state->sample_queue > 0) {
203                 if (!(f = gen_readframe(state)))
204                         return -1;
205
206                 res = ast_write(chan, f);
207                 ast_frfree(f);
208                 if (res < 0) {
209                         ast_chan_log(LOG_WARNING, chan, "Failed to write frame: %s\n", strerror(errno));
210                         return -1;
211                 }
212                 state->sample_queue -= f->samples;
213         }
214
215         return res;
216 }
217
218 static struct ast_generator gen =
219 {
220         alloc: gen_alloc,
221         release: gen_release,
222         generate: gen_generate,
223 };
224
225 static struct playlist_entry *make_entry(const char *filename)
226 {
227         struct playlist_entry *entry;
228
229         if (!(entry = ast_calloc(1, sizeof(*entry) + strlen(filename) + 10))) /* XXX why 10 ? */
230                 return NULL;
231
232         strcpy(entry->filename, filename);
233
234         return entry;
235 }
236
237 static int app_exec(struct ast_channel *chan, void *data)
238 {
239         struct playlist_entry *entry;
240         int child_stdin[2] = { 0,0 };
241         int child_stdout[2] = { 0,0 };
242         int child_stderr[2] = { 0,0 };
243         int res = -1;
244         int gen_active = 0;
245         int pid;
246         char *buf, *command;
247         FILE *child_commands = NULL;
248         FILE *child_errors = NULL;
249         FILE *child_events = NULL;
250         struct ivr_localuser foo = {
251                 .playlist = AST_LIST_HEAD_INIT_VALUE,
252                 .finishlist = AST_LIST_HEAD_INIT_VALUE,
253         };
254         struct ivr_localuser *u = &foo;
255         sigset_t fullset, oldset;
256         AST_DECLARE_APP_ARGS(args,
257                 AST_APP_ARG(cmd)[32];
258         );
259
260         sigfillset(&fullset);
261         pthread_sigmask(SIG_BLOCK, &fullset, &oldset);
262
263         u->abort_current_sound = 0;
264         u->chan = chan;
265
266         if (ast_strlen_zero(data)) {
267                 ast_log(LOG_WARNING, "ExternalIVR requires a command to execute\n");
268                 return -1;
269         }
270
271         buf = ast_strdupa(data);
272         AST_STANDARD_APP_ARGS(args, buf);
273
274         if (pipe(child_stdin)) {
275                 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child input: %s\n", strerror(errno));
276                 goto exit;
277         }
278
279         if (pipe(child_stdout)) {
280                 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child output: %s\n", strerror(errno));
281                 goto exit;
282         }
283
284         if (pipe(child_stderr)) {
285                 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child errors: %s\n", strerror(errno));
286                 goto exit;
287         }
288
289         if (chan->_state != AST_STATE_UP) {
290                 ast_answer(chan);
291         }
292
293         if (ast_activate_generator(chan, &gen, u) < 0) {
294                 ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
295                 goto exit;
296         } else
297                 gen_active = 1;
298
299         pid = fork();
300         if (pid < 0) {
301                 ast_log(LOG_WARNING, "Failed to fork(): %s\n", strerror(errno));
302                 goto exit;
303         }
304
305         if (!pid) {
306                 /* child process */
307                 int i;
308
309                 signal(SIGPIPE, SIG_DFL);
310                 pthread_sigmask(SIG_UNBLOCK, &fullset, NULL);
311
312                 if (ast_opt_high_priority)
313                         ast_set_priority(0);
314
315                 dup2(child_stdin[0], STDIN_FILENO);
316                 dup2(child_stdout[1], STDOUT_FILENO);
317                 dup2(child_stderr[1], STDERR_FILENO);
318                 for (i = STDERR_FILENO + 1; i < 1024; i++)
319                         close(i);
320                 execv(args.cmd[0], args.cmd);
321                 fprintf(stderr, "Failed to execute '%s': %s\n", args.cmd[0], strerror(errno));
322                 _exit(1);
323         } else {
324                 /* parent process */
325                 int child_events_fd = child_stdin[1];
326                 int child_commands_fd = child_stdout[0];
327                 int child_errors_fd = child_stderr[0];
328                 struct ast_frame *f;
329                 int ms;
330                 int exception;
331                 int ready_fd;
332                 int waitfds[2] = { child_errors_fd, child_commands_fd };
333                 struct ast_channel *rchan;
334
335                 pthread_sigmask(SIG_SETMASK, &oldset, NULL);
336
337                 close(child_stdin[0]);
338                 child_stdin[0] = 0;
339                 close(child_stdout[1]);
340                 child_stdout[1] = 0;
341                 close(child_stderr[1]);
342                 child_stderr[1] = 0;
343
344                 if (!(child_events = fdopen(child_events_fd, "w"))) {
345                         ast_chan_log(LOG_WARNING, chan, "Could not open stream for child events\n");
346                         goto exit;
347                 }
348
349                 if (!(child_commands = fdopen(child_commands_fd, "r"))) {
350                         ast_chan_log(LOG_WARNING, chan, "Could not open stream for child commands\n");
351                         goto exit;
352                 }
353
354                 if (!(child_errors = fdopen(child_errors_fd, "r"))) {
355                         ast_chan_log(LOG_WARNING, chan, "Could not open stream for child errors\n");
356                         goto exit;
357                 }
358
359                 setvbuf(child_events, NULL, _IONBF, 0);
360                 setvbuf(child_commands, NULL, _IONBF, 0);
361                 setvbuf(child_errors, NULL, _IONBF, 0);
362
363                 res = 0;
364
365                 while (1) {
366                         if (ast_test_flag(chan, AST_FLAG_ZOMBIE)) {
367                                 ast_chan_log(LOG_NOTICE, chan, "Is a zombie\n");
368                                 res = -1;
369                                 break;
370                         }
371
372                         if (ast_check_hangup(chan)) {
373                                 ast_chan_log(LOG_NOTICE, chan, "Got check_hangup\n");
374                                 send_child_event(child_events, 'H', NULL, chan);
375                                 res = -1;
376                                 break;
377                         }
378
379                         ready_fd = 0;
380                         ms = 100;
381                         errno = 0;
382                         exception = 0;
383
384                         rchan = ast_waitfor_nandfds(&chan, 1, waitfds, 2, &exception, &ready_fd, &ms);
385
386                         if (!AST_LIST_EMPTY(&u->finishlist)) {
387                                 AST_LIST_LOCK(&u->finishlist);
388                                 while ((entry = AST_LIST_REMOVE_HEAD(&u->finishlist, list))) {
389                                         send_child_event(child_events, 'F', entry->filename, chan);
390                                         ast_free(entry);
391                                 }
392                                 AST_LIST_UNLOCK(&u->finishlist);
393                         }
394
395                         if (rchan) {
396                                 /* the channel has something */
397                                 f = ast_read(chan);
398                                 if (!f) {
399                                         ast_chan_log(LOG_NOTICE, chan, "Returned no frame\n");
400                                         send_child_event(child_events, 'H', NULL, chan);
401                                         res = -1;
402                                         break;
403                                 }
404
405                                 if (f->frametype == AST_FRAME_DTMF) {
406                                         send_child_event(child_events, f->subclass, NULL, chan);
407                                         if (u->option_autoclear) {
408                                                 if (!u->abort_current_sound && !u->playing_silence)
409                                                         send_child_event(child_events, 'T', NULL, chan);
410                                                 AST_LIST_LOCK(&u->playlist);
411                                                 while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list))) {
412                                                         send_child_event(child_events, 'D', entry->filename, chan);
413                                                         ast_free(entry);
414                                                 }
415                                                 if (!u->playing_silence)
416                                                         u->abort_current_sound = 1;
417                                                 AST_LIST_UNLOCK(&u->playlist);
418                                         }
419                                 } else if ((f->frametype == AST_FRAME_CONTROL) && (f->subclass == AST_CONTROL_HANGUP)) {
420                                         ast_chan_log(LOG_NOTICE, chan, "Got AST_CONTROL_HANGUP\n");
421                                         send_child_event(child_events, 'H', NULL, chan);
422                                         ast_frfree(f);
423                                         res = -1;
424                                         break;
425                                 }
426                                 ast_frfree(f);
427                         } else if (ready_fd == child_commands_fd) {
428                                 char input[1024];
429
430                                 if (exception || feof(child_commands)) {
431                                         ast_chan_log(LOG_WARNING, chan, "Child process went away\n");
432                                         res = -1;
433                                         break;
434                                 }
435
436                                 if (!fgets(input, sizeof(input), child_commands))
437                                         continue;
438
439                                 command = ast_strip(input);
440
441                                 if (option_debug)
442                                         ast_chan_log(LOG_DEBUG, chan, "got command '%s'\n", input);
443
444                                 if (strlen(input) < 4)
445                                         continue;
446
447                                 if (input[0] == 'S') {
448                                         if (ast_fileexists(&input[2], NULL, u->chan->language) == -1) {
449                                                 ast_chan_log(LOG_WARNING, chan, "Unknown file requested '%s'\n", &input[2]);
450                                                 send_child_event(child_events, 'Z', NULL, chan);
451                                                 strcpy(&input[2], "exception");
452                                         }
453                                         if (!u->abort_current_sound && !u->playing_silence)
454                                                 send_child_event(child_events, 'T', NULL, chan);
455                                         AST_LIST_LOCK(&u->playlist);
456                                         while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list))) {
457                                                 send_child_event(child_events, 'D', entry->filename, chan);
458                                                 ast_free(entry);
459                                         }
460                                         if (!u->playing_silence)
461                                                 u->abort_current_sound = 1;
462                                         entry = make_entry(&input[2]);
463                                         if (entry)
464                                                 AST_LIST_INSERT_TAIL(&u->playlist, entry, list);
465                                         AST_LIST_UNLOCK(&u->playlist);
466                                 } else if (input[0] == 'A') {
467                                         if (ast_fileexists(&input[2], NULL, u->chan->language) == -1) {
468                                                 ast_chan_log(LOG_WARNING, chan, "Unknown file requested '%s'\n", &input[2]);
469                                                 send_child_event(child_events, 'Z', NULL, chan);
470                                                 strcpy(&input[2], "exception");
471                                         }
472                                         entry = make_entry(&input[2]);
473                                         if (entry) {
474                                                 AST_LIST_LOCK(&u->playlist);
475                                                 AST_LIST_INSERT_TAIL(&u->playlist, entry, list);
476                                                 AST_LIST_UNLOCK(&u->playlist);
477                                         }
478                                 } else if (input[0] == 'E') {
479                                         ast_chan_log(LOG_NOTICE, chan, "Exiting: %s\n", &input[2]);
480                                         send_child_event(child_events, 'E', NULL, chan);
481                                         res = 0;
482                                         break;
483                                 } else if (input[0] == 'H') {
484                                         ast_chan_log(LOG_NOTICE, chan, "Hanging up: %s\n", &input[2]);
485                                         send_child_event(child_events, 'H', NULL, chan);
486                                         res = -1;
487                                         break;
488                                 } else if (input[0] == 'O') {
489                                         if (!strcasecmp(&input[2], "autoclear"))
490                                                 u->option_autoclear = 1;
491                                         else if (!strcasecmp(&input[2], "noautoclear"))
492                                                 u->option_autoclear = 0;
493                                         else
494                                                 ast_chan_log(LOG_WARNING, chan, "Unknown option requested '%s'\n", &input[2]);
495                                 } else if (input[0] == 'V') {
496                                         char *c;
497                                         c = strchr(&input[2], '=');
498                                         if (!c) {
499                                                 send_child_event(child_events, 'Z', NULL, chan);
500                                         } else {
501                                                 *c++ = '\0';
502                                                 pbx_builtin_setvar_helper(chan, &input[2], c);
503                                         }
504                                 }
505                         } else if (ready_fd == child_errors_fd) {
506                                 char input[1024];
507
508                                 if (exception || feof(child_errors)) {
509                                         ast_chan_log(LOG_WARNING, chan, "Child process went away\n");
510                                         res = -1;
511                                         break;
512                                 }
513
514                                 if (fgets(input, sizeof(input), child_errors)) {
515                                         command = ast_strip(input);
516                                         ast_chan_log(LOG_NOTICE, chan, "stderr: %s\n", command);
517                                 }
518                         } else if ((ready_fd < 0) && ms) {
519                                 if (errno == 0 || errno == EINTR)
520                                         continue;
521
522                                 ast_chan_log(LOG_WARNING, chan, "Wait failed (%s)\n", strerror(errno));
523                                 break;
524                         }
525                 }
526         }
527
528  exit:
529         if (gen_active)
530                 ast_deactivate_generator(chan);
531
532         if (child_events)
533                 fclose(child_events);
534
535         if (child_commands)
536                 fclose(child_commands);
537
538         if (child_errors)
539                 fclose(child_errors);
540
541         if (child_stdin[0])
542                 close(child_stdin[0]);
543
544         if (child_stdin[1])
545                 close(child_stdin[1]);
546
547         if (child_stdout[0])
548                 close(child_stdout[0]);
549
550         if (child_stdout[1])
551                 close(child_stdout[1]);
552
553         if (child_stderr[0])
554                 close(child_stderr[0]);
555
556         if (child_stderr[1])
557                 close(child_stderr[1]);
558
559         while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list)))
560                 ast_free(entry);
561
562         return res;
563 }
564
565 static int unload_module(void)
566 {
567         return ast_unregister_application(app);
568 }
569
570 static int load_module(void)
571 {
572         return ast_register_application(app, app_exec, synopsis, descrip);
573 }
574
575 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "External IVR Interface Application");