(closes issue #11825)
[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 int eivr_comm(struct ast_channel *chan, struct ivr_localuser *u, 
89               int eivr_events_fd, int eivr_commands_fd, int eivr_errors_fd, 
90               const char *args);
91
92 static void send_eivr_event(FILE *handle, const char event, const char *data,
93         const struct ast_channel *chan)
94 {
95         char tmp[256];
96
97         if (!data) {
98                 snprintf(tmp, sizeof(tmp), "%c,%10d", event, (int)time(NULL));
99         } else {
100                 snprintf(tmp, sizeof(tmp), "%c,%10d,%s", event, (int)time(NULL), data);
101         }
102
103         fprintf(handle, "%s\n", tmp);
104         if (option_debug)
105                 ast_chan_log(LOG_DEBUG, chan, "sent '%s'\n", tmp);
106 }
107
108 static void *gen_alloc(struct ast_channel *chan, void *params)
109 {
110         struct ivr_localuser *u = params;
111         struct gen_state *state;
112
113         if (!(state = ast_calloc(1, sizeof(*state))))
114                 return NULL;
115
116         state->u = u;
117
118         return state;
119 }
120
121 static void gen_closestream(struct gen_state *state)
122 {
123         if (!state->stream)
124                 return;
125
126         ast_closestream(state->stream);
127         state->u->chan->stream = NULL;
128         state->stream = NULL;
129 }
130
131 static void gen_release(struct ast_channel *chan, void *data)
132 {
133         struct gen_state *state = data;
134
135         gen_closestream(state);
136         ast_free(data);
137 }
138
139 /* caller has the playlist locked */
140 static int gen_nextfile(struct gen_state *state)
141 {
142         struct ivr_localuser *u = state->u;
143         char *file_to_stream;
144
145         u->abort_current_sound = 0;
146         u->playing_silence = 0;
147         gen_closestream(state);
148
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;
153                 } else {
154                         file_to_stream = "silence/10";
155                         u->playing_silence = 1;
156                 }
157
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) {
161                                 continue;
162                         } else {
163                                 break;
164                         }
165                 }
166         }
167
168         return (!state->stream);
169 }
170
171 static struct ast_frame *gen_readframe(struct gen_state *state)
172 {
173         struct ast_frame *f = NULL;
174         struct ivr_localuser *u = state->u;
175
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);
180                 gen_nextfile(state);
181                 AST_LIST_UNLOCK(&u->playlist);
182         }
183
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;
190                 }
191                 if (!gen_nextfile(state))
192                         f = ast_readframe(state->stream);
193         }
194
195         return f;
196 }
197
198 static int gen_generate(struct ast_channel *chan, void *data, int len, int samples)
199 {
200         struct gen_state *state = data;
201         struct ast_frame *f = NULL;
202         int res = 0;
203
204         state->sample_queue += samples;
205
206         while (state->sample_queue > 0) {
207                 if (!(f = gen_readframe(state)))
208                         return -1;
209
210                 res = ast_write(chan, f);
211                 ast_frfree(f);
212                 if (res < 0) {
213                         ast_chan_log(LOG_WARNING, chan, "Failed to write frame: %s\n", strerror(errno));
214                         return -1;
215                 }
216                 state->sample_queue -= f->samples;
217         }
218
219         return res;
220 }
221
222 static struct ast_generator gen =
223 {
224         alloc: gen_alloc,
225         release: gen_release,
226         generate: gen_generate,
227 };
228
229 static void ast_eivr_getvariable(struct ast_channel *chan, char *data, char *outbuf, int outbuflen)
230 {
231         // original input data: "G,var1,var2,"
232         // data passed as "data":  "var1,var2"
233         char *inbuf, *variable;
234
235         const char *value;
236         char *saveptr;
237         int j;
238
239         outbuf[0] = 0;
240
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;
247                         }
248                         break;
249                 }
250                 
251                 value = pbx_builtin_getvar_helper(chan, variable);
252                 if(!value)
253                         value = "";
254                 strncat(outbuf,variable,outbuflen);
255                 strncat(outbuf,"=",outbuflen);
256                 strncat(outbuf,value,outbuflen);
257                 strncat(outbuf,",",outbuflen);
258         }
259 };
260
261 static void ast_eivr_setvariable(struct ast_channel *chan, char *data)
262 {
263         char buf[1024];
264         char *value;
265
266         char *inbuf, *variable;
267
268         char *saveptr;
269         int j;
270
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);
274                 if(variable) {
275                         //variable contains "varname=value"
276                         strncpy(buf, variable, sizeof(buf));
277                         value = strchr(buf, '=');
278                         if(!value) 
279                                 value="";
280                         else {
281                                 value[0] = 0;
282                                 value++;
283                         }
284                         pbx_builtin_setvar_helper(chan, buf, value);
285                 }
286                 else break;
287
288         }
289
290 };
291
292 static struct playlist_entry *make_entry(const char *filename)
293 {
294         struct playlist_entry *entry;
295
296         if (!(entry = ast_calloc(1, sizeof(*entry) + strlen(filename) + 10))) /* XXX why 10 ? */
297                 return NULL;
298
299         strcpy(entry->filename, filename);
300
301         return entry;
302 }
303
304 static int app_exec(struct ast_channel *chan, void *data)
305 {
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 };
310         int res = -1;
311         int gen_active = 0;
312         int pid;
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,
317         };
318         struct ivr_localuser *u = &foo;
319         sigset_t fullset, oldset;
320         AST_DECLARE_APP_ARGS(args,
321                 AST_APP_ARG(cmd)[32];
322         );
323
324         sigfillset(&fullset);
325         pthread_sigmask(SIG_BLOCK, &fullset, &oldset);
326
327         u->abort_current_sound = 0;
328         u->chan = chan;
329
330         if (ast_strlen_zero(data)) {
331                 ast_log(LOG_WARNING, "ExternalIVR requires a command to execute\n");
332                 return -1;
333         }
334
335         buf = ast_strdupa(data);
336         AST_STANDARD_APP_ARGS(args, buf);
337
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] = '|';
342         
343         if (pipe(child_stdin)) {
344                 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child input: %s\n", strerror(errno));
345                 goto exit;
346         }
347         if (pipe(child_stdout)) {
348                 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child output: %s\n", strerror(errno));
349                 goto exit;
350         }
351         if (pipe(child_stderr)) {
352                 ast_chan_log(LOG_WARNING, chan, "Could not create pipe for child errors: %s\n", strerror(errno));
353                 goto exit;
354         }
355         if (chan->_state != AST_STATE_UP) {
356                 ast_answer(chan);
357         }
358         if (ast_activate_generator(chan, &gen, u) < 0) {
359                 ast_chan_log(LOG_WARNING, chan, "Failed to activate generator\n");
360                 goto exit;
361         } else
362                 gen_active = 1;
363
364         pid = fork();
365         if (pid < 0) {
366                 ast_log(LOG_WARNING, "Failed to fork(): %s\n", strerror(errno));
367                 goto exit;
368         }
369
370         if (!pid) {
371                 /* child process */
372                 int i;
373
374                 signal(SIGPIPE, SIG_DFL);
375                 pthread_sigmask(SIG_UNBLOCK, &fullset, NULL);
376
377                 if (ast_opt_high_priority)
378                         ast_set_priority(0);
379
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++)
384                         close(i);
385                 execv(args.cmd[0], args.cmd);
386                 fprintf(stderr, "Failed to execute '%s': %s\n", args.cmd[0], strerror(errno));
387                 _exit(1);
388         } else {
389                 /* parent process */
390
391                 close(child_stdin[0]);
392                 child_stdin[0] = 0;
393                 close(child_stdout[1]);
394                 child_stdout[1] = 0;
395                 close(child_stderr[1]);
396                 child_stderr[1] = 0;
397                 res = eivr_comm(chan, u, child_stdin[1], child_stdout[0], child_stderr[0], pipe_delim_argbuf);
398
399                 exit:
400                 if (gen_active)
401                         ast_deactivate_generator(chan);
402
403                 if (child_stdin[0])
404                         close(child_stdin[0]);
405
406                 if (child_stdin[1])
407                         close(child_stdin[1]);
408
409                 if (child_stdout[0])
410                         close(child_stdout[0]);
411
412                 if (child_stdout[1])
413                         close(child_stdout[1]);
414
415                 if (child_stderr[0])
416                         close(child_stderr[0]);
417
418                 if (child_stderr[1])
419                         close(child_stderr[1]);
420
421                 while ((entry = AST_LIST_REMOVE_HEAD(&u->playlist, list)))
422                         ast_free(entry);
423
424                 return res;
425         }
426 }
427
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, 
430                                 const char *args)
431 {
432         struct playlist_entry *entry;
433         struct ast_frame *f;
434         int ms;
435         int exception;
436         int ready_fd;
437         int waitfds[2] = { eivr_commands_fd, eivr_errors_fd };
438         struct ast_channel *rchan;
439         char *command;
440         int res = -1;
441   
442         FILE *eivr_commands = NULL;
443         FILE *eivr_errors = NULL;
444         FILE *eivr_events = NULL;
445  
446         if (!(eivr_events = fdopen(eivr_events_fd, "w"))) {
447                 ast_chan_log(LOG_WARNING, chan, "Could not open stream to send events\n");
448                 goto exit;
449         }
450         if (!(eivr_commands = fdopen(eivr_commands_fd, "r"))) {
451                 ast_chan_log(LOG_WARNING, chan, "Could not open stream to receive commands\n");
452                 goto exit;
453         }
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");
457                         goto exit;
458                 }
459         }
460  
461         setvbuf(eivr_events, NULL, _IONBF, 0);
462         setvbuf(eivr_commands, NULL, _IONBF, 0);
463         if(eivr_errors)
464                 setvbuf(eivr_errors, NULL, _IONBF, 0);
465
466         res = 0;
467  
468         while (1) {
469                 if (ast_test_flag(chan, AST_FLAG_ZOMBIE)) {
470                         ast_chan_log(LOG_NOTICE, chan, "Is a zombie\n");
471                         res = -1;
472                         break;
473                 }
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);
477                         res = -1;
478                         break;
479                 }
480  
481                 ready_fd = 0;
482                 ms = 100;
483                 errno = 0;
484                 exception = 0;
485  
486                 rchan = ast_waitfor_nandfds(&chan, 1, waitfds, 2, &exception, &ready_fd, &ms);
487  
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);
492                                 ast_free(entry);
493                         }
494                         AST_LIST_UNLOCK(&u->finishlist);
495                 }
496  
497                 if (rchan) {
498                         /* the channel has something */
499                         f = ast_read(chan);
500                         if (!f) {
501                                 ast_chan_log(LOG_NOTICE, chan, "Returned no frame\n");
502                                 send_eivr_event(eivr_events, 'H', NULL, chan);
503                                 res = -1;
504                                 break;
505                         }
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);
514                                                 ast_free(entry);
515                                         }
516                                         if (!u->playing_silence)
517                                                 u->abort_current_sound = 1;
518                                         AST_LIST_UNLOCK(&u->playlist);
519                                 }
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);
523                                 ast_frfree(f);
524                                 res = -1;
525                                 break;
526                         }
527                         ast_frfree(f);
528                 } else if (ready_fd == eivr_commands_fd) {
529                         char input[1024];
530  
531                         if (exception || feof(eivr_commands)) {
532                                 ast_chan_log(LOG_WARNING, chan, "Child process went away\n");
533                                 res = -1;
534                                 break;
535                         }
536   
537                         if (!fgets(input, sizeof(input), eivr_commands))
538                                 continue;
539  
540                         command = ast_strip(input);
541   
542                         if (option_debug)
543                                 ast_chan_log(LOG_DEBUG, chan, "got command '%s'\n", input);
544   
545                         if (strlen(input) < 4)
546                                 continue;
547   
548                         if (input[0] == 'P') {
549                                 send_eivr_event(eivr_events, 'P', args, chan);
550  
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");
556                                 }
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);
562                                         ast_free(entry);
563                                 }
564                                 if (!u->playing_silence)
565                                         u->abort_current_sound = 1;
566                                 entry = make_entry(&input[2]);
567                                 if (entry)
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");
575                                 }
576                                 entry = make_entry(&input[2]);
577                                 if (entry) {
578                                         AST_LIST_LOCK(&u->playlist);
579                                         AST_LIST_INSERT_TAIL(&u->playlist, entry, list);
580                                         AST_LIST_UNLOCK(&u->playlist);
581                                 }
582                         } else if (input[0] == 'G') {
583                                 // A get variable message:  "G,variable1,variable2,..."
584                                 char response[2048];
585                                 ast_chan_log(LOG_NOTICE, chan, "Getting a Variable out of the channel: %s\n", &input[2]);
586                                 ast_eivr_getvariable(chan, &input[2], response, sizeof(response));
587                                 send_eivr_event(eivr_events, 'G', response, chan);
588                         } else if (input[0] == 'V') {
589                                 // A set variable message:  "V,variablename=foo"
590                                 ast_chan_log(LOG_NOTICE, chan, "Setting a Variable up: %s\n", &input[2]);
591                                 ast_eivr_setvariable(chan, &input[2]);
592                         } else if (input[0] == 'L') {
593                                 ast_chan_log(LOG_NOTICE, chan, "Log message from EIVR: %s\n", &input[2]);
594                         } else if (input[0] == 'X') {
595                                 ast_chan_log(LOG_NOTICE, chan, "Exiting ExternalIVR: %s\n", &input[2]);
596                                 //TODO: add deprecation debug message for X command here
597                                 res = 0;
598                                 break;
599                         } else if (input[0] == 'E') {
600                                 ast_chan_log(LOG_NOTICE, chan, "Exiting: %s\n", &input[2]);
601                                 send_eivr_event(eivr_events, 'E', NULL, chan);
602                                 res = 0;
603                                 break;
604                         } else if (input[0] == 'H') {
605                                 ast_chan_log(LOG_NOTICE, chan, "Hanging up: %s\n", &input[2]);
606                                 send_eivr_event(eivr_events, 'H', NULL, chan);
607                                 res = -1;
608                                 break;
609                         } else if (input[0] == 'O') {
610                                 if (!strcasecmp(&input[2], "autoclear"))
611                                         u->option_autoclear = 1;
612                                 else if (!strcasecmp(&input[2], "noautoclear"))
613                                         u->option_autoclear = 0;
614                                 else
615                                         ast_chan_log(LOG_WARNING, chan, "Unknown option requested '%s'\n", &input[2]);
616                         }
617                 } else if (eivr_errors_fd && ready_fd == eivr_errors_fd) {
618                         char input[1024];
619   
620                         if (exception || feof(eivr_errors)) {
621                                 ast_chan_log(LOG_WARNING, chan, "Child process went away\n");
622                                 res = -1;
623                                 break;
624                         }
625                         if (fgets(input, sizeof(input), eivr_errors)) {
626                                 command = ast_strip(input);
627                                 ast_chan_log(LOG_NOTICE, chan, "stderr: %s\n", command);
628                         }
629                 } else if ((ready_fd < 0) && ms) { 
630                         if (errno == 0 || errno == EINTR)
631                                 continue;
632  
633                         ast_chan_log(LOG_WARNING, chan, "Wait failed (%s)\n", strerror(errno));
634                         break;
635                 }
636         }
637   
638  
639 exit:
640  
641         if (eivr_events)
642                 fclose(eivr_events);
643  
644         if (eivr_commands)
645                 fclose(eivr_commands);
646
647         if (eivr_errors)
648                 fclose(eivr_errors);
649   
650         return res;
651  
652   }
653
654 static int unload_module(void)
655 {
656         return ast_unregister_application(app);
657 }
658
659 static int load_module(void)
660 {
661         return ast_register_application(app, app_exec, synopsis, descrip);
662 }
663
664 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "External IVR Interface Application");