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