source: branches/3.2/mindi-busybox/miscutils/chat.c @ 3232

Last change on this file since 3232 was 3232, checked in by bruno, 5 years ago
  • Update mindi-busybox to 1.21.1
  • Property svn:eol-style set to native
File size: 11.0 KB
Line 
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
28enum {
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
42static 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
51static 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
102int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
103int 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 > 0) {
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}
Note: See TracBrowser for help on using the repository browser.