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