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