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