Convert all miscutils/* applets to "new style" applet definitions
[oweals/busybox.git] / miscutils / chat.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * bare bones chat utility
4  * inspired by ppp's chat
5  *
6  * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
7  *
8  * Licensed under GPLv2, see file LICENSE in this source tree.
9  */
10 //config:config CHAT
11 //config:       bool "chat"
12 //config:       default y
13 //config:       help
14 //config:         Simple chat utility.
15 //config:
16 //config:config FEATURE_CHAT_NOFAIL
17 //config:       bool "Enable NOFAIL expect strings"
18 //config:       depends on CHAT
19 //config:       default y
20 //config:       help
21 //config:         When enabled expect strings which are started with a dash trigger
22 //config:         no-fail mode. That is when expectation is not met within timeout
23 //config:         the script is not terminated but sends next SEND string and waits
24 //config:         for next EXPECT string. This allows to compose far more flexible
25 //config:         scripts.
26 //config:
27 //config:config FEATURE_CHAT_TTY_HIFI
28 //config:       bool "Force STDIN to be a TTY"
29 //config:       depends on CHAT
30 //config:       default n
31 //config:       help
32 //config:         Original chat always treats STDIN as a TTY device and sets for it
33 //config:         so-called raw mode. This option turns on such behaviour.
34 //config:
35 //config:config FEATURE_CHAT_IMPLICIT_CR
36 //config:       bool "Enable implicit Carriage Return"
37 //config:       depends on CHAT
38 //config:       default y
39 //config:       help
40 //config:         When enabled make chat to terminate all SEND strings with a "\r"
41 //config:         unless "\c" is met anywhere in the string.
42 //config:
43 //config:config FEATURE_CHAT_SWALLOW_OPTS
44 //config:       bool "Swallow options"
45 //config:       depends on CHAT
46 //config:       default y
47 //config:       help
48 //config:         Busybox chat require no options. To make it not fail when used
49 //config:         in place of original chat (which has a bunch of options) turn
50 //config:         this on.
51 //config:
52 //config:config FEATURE_CHAT_SEND_ESCAPES
53 //config:       bool "Support weird SEND escapes"
54 //config:       depends on CHAT
55 //config:       default y
56 //config:       help
57 //config:         Original chat uses some escape sequences in SEND arguments which
58 //config:         are not sent to device but rather performs special actions.
59 //config:         E.g. "\K" means to send a break sequence to device.
60 //config:         "\d" delays execution for a second, "\p" -- for a 1/100 of second.
61 //config:         Before turning this option on think twice: do you really need them?
62 //config:
63 //config:config FEATURE_CHAT_VAR_ABORT_LEN
64 //config:       bool "Support variable-length ABORT conditions"
65 //config:       depends on CHAT
66 //config:       default y
67 //config:       help
68 //config:         Original chat uses fixed 50-bytes length ABORT conditions. Say N here.
69 //config:
70 //config:config FEATURE_CHAT_CLR_ABORT
71 //config:       bool "Support revoking of ABORT conditions"
72 //config:       depends on CHAT
73 //config:       default y
74 //config:       help
75 //config:         Support CLR_ABORT directive.
76
77 //applet:IF_CHAT(APPLET(chat, BB_DIR_USR_SBIN, BB_SUID_DROP))
78
79 //kbuild:lib-$(CONFIG_CHAT) += chat.o
80
81 //usage:#define chat_trivial_usage
82 //usage:       "EXPECT [SEND [EXPECT [SEND...]]]"
83 //usage:#define chat_full_usage "\n\n"
84 //usage:       "Useful for interacting with a modem connected to stdin/stdout.\n"
85 //usage:       "A script consists of one or more \"expect-send\" pairs of strings,\n"
86 //usage:       "each pair is a pair of arguments. Example:\n"
87 //usage:       "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"
88
89 #include "libbb.h"
90 #include "common_bufsiz.h"
91
92 // default timeout: 45 sec
93 #define DEFAULT_CHAT_TIMEOUT 45*1000
94 // max length of "abort string",
95 // i.e. device reply which causes termination
96 #define MAX_ABORT_LEN 50
97
98 // possible exit codes
99 enum {
100         ERR_OK = 0,     // all's well
101         ERR_MEM,        // read too much while expecting
102         ERR_IO,         // signalled or I/O error
103         ERR_TIMEOUT,    // timed out while expecting
104         ERR_ABORT,      // first abort condition was met
105 //      ERR_ABORT2,     // second abort condition was met
106 //      ...
107 };
108
109 // exit code
110 #define exitcode bb_got_signal
111
112 // trap for critical signals
113 static void signal_handler(UNUSED_PARAM int signo)
114 {
115         // report I/O error condition
116         exitcode = ERR_IO;
117 }
118
119 #if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
120 #define unescape(s, nocr) unescape(s)
121 #endif
122 static size_t unescape(char *s, int *nocr)
123 {
124         char *start = s;
125         char *p = s;
126
127         while (*s) {
128                 char c = *s;
129                 // do we need special processing?
130                 // standard escapes + \s for space and \N for \0
131                 // \c inhibits terminating \r for commands and is noop for expects
132                 if ('\\' == c) {
133                         c = *++s;
134                         if (c) {
135 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
136                                 if ('c' == c) {
137                                         *nocr = 1;
138                                         goto next;
139                                 }
140 #endif
141                                 if ('N' == c) {
142                                         c = '\0';
143                                 } else if ('s' == c) {
144                                         c = ' ';
145 #if ENABLE_FEATURE_CHAT_NOFAIL
146                                 // unescape leading dash only
147                                 // TODO: and only for expect, not command string
148                                 } else if ('-' == c && (start + 1 == s)) {
149                                         //c = '-';
150 #endif
151                                 } else {
152                                         c = bb_process_escape_sequence((const char **)&s);
153                                         s--;
154                                 }
155                         }
156                 // ^A becomes \001, ^B -- \002 and so on...
157                 } else if ('^' == c) {
158                         c = *++s-'@';
159                 }
160                 // put unescaped char
161                 *p++ = c;
162 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
163  next:
164 #endif
165                 // next char
166                 s++;
167         }
168         *p = '\0';
169
170         return p - start;
171 }
172
173 int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
174 int chat_main(int argc UNUSED_PARAM, char **argv)
175 {
176         int record_fd = -1;
177         bool echo = 0;
178         // collection of device replies which cause unconditional termination
179         llist_t *aborts = NULL;
180         // inactivity period
181         int timeout = DEFAULT_CHAT_TIMEOUT;
182         // maximum length of abort string
183 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
184         size_t max_abort_len = 0;
185 #else
186 #define max_abort_len MAX_ABORT_LEN
187 #endif
188 #if ENABLE_FEATURE_CHAT_TTY_HIFI
189         struct termios tio0, tio;
190 #endif
191         // directive names
192         enum {
193                 DIR_HANGUP = 0,
194                 DIR_ABORT,
195 #if ENABLE_FEATURE_CHAT_CLR_ABORT
196                 DIR_CLR_ABORT,
197 #endif
198                 DIR_TIMEOUT,
199                 DIR_ECHO,
200                 DIR_SAY,
201                 DIR_RECORD,
202         };
203
204         // make x* functions fail with correct exitcode
205         xfunc_error_retval = ERR_IO;
206
207         // trap vanilla signals to prevent process from being killed suddenly
208         bb_signals(0
209                 + (1 << SIGHUP)
210                 + (1 << SIGINT)
211                 + (1 << SIGTERM)
212                 + (1 << SIGPIPE)
213                 , signal_handler);
214
215 #if ENABLE_FEATURE_CHAT_TTY_HIFI
216         tcgetattr(STDIN_FILENO, &tio);
217         tio0 = tio;
218         cfmakeraw(&tio);
219         tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
220 #endif
221
222 #if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
223         getopt32(argv, "vVsSE");
224         argv += optind;
225 #else
226         argv++; // goto first arg
227 #endif
228         // handle chat expect-send pairs
229         while (*argv) {
230                 // directive given? process it
231                 int key = index_in_strings(
232                         "HANGUP\0" "ABORT\0"
233 #if ENABLE_FEATURE_CHAT_CLR_ABORT
234                         "CLR_ABORT\0"
235 #endif
236                         "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
237                         , *argv
238                 );
239                 if (key >= 0) {
240                         // cache directive value
241                         char *arg = *++argv;
242                         // OFF -> 0, anything else -> 1
243                         bool onoff = (0 != strcmp("OFF", arg));
244                         // process directive
245                         if (DIR_HANGUP == key) {
246                                 // turn SIGHUP on/off
247                                 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
248                         } else if (DIR_ABORT == key) {
249                                 // append the string to abort conditions
250 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
251                                 size_t len = strlen(arg);
252                                 if (len > max_abort_len)
253                                         max_abort_len = len;
254 #endif
255                                 llist_add_to_end(&aborts, arg);
256 #if ENABLE_FEATURE_CHAT_CLR_ABORT
257                         } else if (DIR_CLR_ABORT == key) {
258                                 llist_t *l;
259                                 // remove the string from abort conditions
260                                 // N.B. gotta refresh maximum length too...
261 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
262                                 max_abort_len = 0;
263 # endif
264                                 for (l = aborts; l; l = l->link) {
265 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
266                                         size_t len = strlen(l->data);
267 # endif
268                                         if (strcmp(arg, l->data) == 0) {
269                                                 llist_unlink(&aborts, l);
270                                                 continue;
271                                         }
272 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
273                                         if (len > max_abort_len)
274                                                 max_abort_len = len;
275 # endif
276                                 }
277 #endif
278                         } else if (DIR_TIMEOUT == key) {
279                                 // set new timeout
280                                 // -1 means OFF
281                                 timeout = atoi(arg) * 1000;
282                                 // 0 means default
283                                 // >0 means value in msecs
284                                 if (!timeout)
285                                         timeout = DEFAULT_CHAT_TIMEOUT;
286                         } else if (DIR_ECHO == key) {
287                                 // turn echo on/off
288                                 // N.B. echo means dumping device input/output to stderr
289                                 echo = onoff;
290                         } else if (DIR_RECORD == key) {
291                                 // turn record on/off
292                                 // N.B. record means dumping device input to a file
293                                         // close previous record_fd
294                                 if (record_fd > 0)
295                                         close(record_fd);
296                                 // N.B. do we have to die here on open error?
297                                 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
298                         } else if (DIR_SAY == key) {
299                                 // just print argument verbatim
300                                 // TODO: should we use full_write() to avoid unistd/stdio conflict?
301                                 bb_error_msg("%s", arg);
302                         }
303                         // next, please!
304                         argv++;
305                 // ordinary expect-send pair!
306                 } else {
307                         //-----------------------
308                         // do expect
309                         //-----------------------
310                         int expect_len;
311                         size_t buf_len = 0;
312                         size_t max_len = max_abort_len;
313
314                         struct pollfd pfd;
315 #if ENABLE_FEATURE_CHAT_NOFAIL
316                         int nofail = 0;
317 #endif
318                         char *expect = *argv++;
319
320                         // sanity check: shall we really expect something?
321                         if (!expect)
322                                 goto expect_done;
323
324 #if ENABLE_FEATURE_CHAT_NOFAIL
325                         // if expect starts with -
326                         if ('-' == *expect) {
327                                 // swallow -
328                                 expect++;
329                                 // and enter nofail mode
330                                 nofail++;
331                         }
332 #endif
333
334 #ifdef ___TEST___BUF___ // test behaviour with a small buffer
335 #       undef COMMON_BUFSIZE
336 #       define COMMON_BUFSIZE 6
337 #endif
338                         // expand escape sequences in expect
339                         expect_len = unescape(expect, &expect_len /*dummy*/);
340                         if (expect_len > max_len)
341                                 max_len = expect_len;
342                         // sanity check:
343                         // we should expect more than nothing but not more than input buffer
344                         // TODO: later we'll get rid of fixed-size buffer
345                         if (!expect_len)
346                                 goto expect_done;
347                         if (max_len >= COMMON_BUFSIZE) {
348                                 exitcode = ERR_MEM;
349                                 goto expect_done;
350                         }
351
352                         // get reply
353                         pfd.fd = STDIN_FILENO;
354                         pfd.events = POLLIN;
355                         while (!exitcode
356                             && poll(&pfd, 1, timeout) > 0
357                             && (pfd.revents & POLLIN)
358                         ) {
359                                 llist_t *l;
360                                 ssize_t delta;
361 #define buf bb_common_bufsiz1
362                                 setup_common_bufsiz();
363
364                                 // read next char from device
365                                 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
366                                         // dump device input if RECORD fname
367                                         if (record_fd > 0) {
368                                                 full_write(record_fd, buf+buf_len, 1);
369                                         }
370                                         // dump device input if ECHO ON
371                                         if (echo) {
372 //                                              if (buf[buf_len] < ' ') {
373 //                                                      full_write(STDERR_FILENO, "^", 1);
374 //                                                      buf[buf_len] += '@';
375 //                                              }
376                                                 full_write(STDERR_FILENO, buf+buf_len, 1);
377                                         }
378                                         buf_len++;
379                                         // move input frame if we've reached higher bound
380                                         if (buf_len > COMMON_BUFSIZE) {
381                                                 memmove(buf, buf+buf_len-max_len, max_len);
382                                                 buf_len = max_len;
383                                         }
384                                 }
385                                 // N.B. rule of thumb: values being looked for can
386                                 // be found only at the end of input buffer
387                                 // this allows to get rid of strstr() and memmem()
388
389                                 // TODO: make expect and abort strings processed uniformly
390                                 // abort condition is met? -> bail out
391                                 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
392                                         size_t len = strlen(l->data);
393                                         delta = buf_len-len;
394                                         if (delta >= 0 && !memcmp(buf+delta, l->data, len))
395                                                 goto expect_done;
396                                 }
397                                 exitcode = ERR_OK;
398
399                                 // expected reply received? -> goto next command
400                                 delta = buf_len - expect_len;
401                                 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
402                                         goto expect_done;
403 #undef buf
404                         } /* while (have data) */
405
406                         // device timed out or unexpected reply received
407                         exitcode = ERR_TIMEOUT;
408  expect_done:
409 #if ENABLE_FEATURE_CHAT_NOFAIL
410                         // on success and when in nofail mode
411                         // we should skip following subsend-subexpect pairs
412                         if (nofail) {
413                                 if (!exitcode) {
414                                         // find last send before non-dashed expect
415                                         while (*argv && argv[1] && '-' == argv[1][0])
416                                                 argv += 2;
417                                         // skip the pair
418                                         // N.B. do we really need this?!
419                                         if (!*argv++ || !*argv++)
420                                                 break;
421                                 }
422                                 // nofail mode also clears all but IO errors (or signals)
423                                 if (ERR_IO != exitcode)
424                                         exitcode = ERR_OK;
425                         }
426 #endif
427                         // bail out unless we expected successfully
428                         if (exitcode)
429                                 break;
430
431                         //-----------------------
432                         // do send
433                         //-----------------------
434                         if (*argv) {
435 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
436                                 int nocr = 0; // inhibit terminating command with \r
437 #endif
438                                 char *loaded = NULL; // loaded command
439                                 size_t len;
440                                 char *buf = *argv++;
441
442                                 // if command starts with @
443                                 // load "real" command from file named after @
444                                 if ('@' == *buf) {
445                                         // skip the @ and any following white-space
446                                         trim(++buf);
447                                         buf = loaded = xmalloc_xopen_read_close(buf, NULL);
448                                 }
449                                 // expand escape sequences in command
450                                 len = unescape(buf, &nocr);
451
452                                 // send command
453                                 alarm(timeout);
454                                 pfd.fd = STDOUT_FILENO;
455                                 pfd.events = POLLOUT;
456                                 while (len && !exitcode
457                                     && poll(&pfd, 1, -1) > 0
458                                     && (pfd.revents & POLLOUT)
459                                 ) {
460 #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
461                                         // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
462                                         // "\\K" means send BREAK
463                                         char c = *buf;
464                                         if ('\\' == c) {
465                                                 c = *++buf;
466                                                 if ('d' == c) {
467                                                         sleep(1);
468                                                         len--;
469                                                         continue;
470                                                 }
471                                                 if ('p' == c) {
472                                                         usleep(10000);
473                                                         len--;
474                                                         continue;
475                                                 }
476                                                 if ('K' == c) {
477                                                         tcsendbreak(STDOUT_FILENO, 0);
478                                                         len--;
479                                                         continue;
480                                                 }
481                                                 buf--;
482                                         }
483                                         if (safe_write(STDOUT_FILENO, buf, 1) != 1)
484                                                 break;
485                                         len--;
486                                         buf++;
487 #else
488                                         len -= full_write(STDOUT_FILENO, buf, len);
489 #endif
490                                 } /* while (can write) */
491                                 alarm(0);
492
493                                 // report I/O error if there still exists at least one non-sent char
494                                 if (len)
495                                         exitcode = ERR_IO;
496
497                                 // free loaded command (if any)
498                                 if (loaded)
499                                         free(loaded);
500 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
501                                 // or terminate command with \r (if not inhibited)
502                                 else if (!nocr)
503                                         xwrite(STDOUT_FILENO, "\r", 1);
504 #endif
505                                 // bail out unless we sent command successfully
506                                 if (exitcode)
507                                         break;
508                         } /* if (*argv) */
509                 }
510         } /* while (*argv) */
511
512 #if ENABLE_FEATURE_CHAT_TTY_HIFI
513         tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
514 #endif
515
516         return exitcode;
517 }