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