123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- /* vi: set sw=4 ts=4: */
- /*
- * bare bones chat utility
- * inspired by ppp's chat
- *
- * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
- *
- * Licensed under GPLv2, see file LICENSE in this tarball for details.
- */
- #include "libbb.h"
- /*
- #define ENABLE_FEATURE_CHAT_NOFAIL 1 // +126 bytes
- #define ENABLE_FEATURE_CHAT_TTY_HIFI 0 // + 70 bytes
- #define ENABLE_FEATURE_CHAT_IMPLICIT_CR 1 // + 44 bytes
- #define ENABLE_FEATURE_CHAT_SEND_ESCAPES 0 // +103 bytes
- #define ENABLE_FEATURE_CHAT_VAR_ABORT_LEN 0 // + 70 bytes
- #define ENABLE_FEATURE_CHAT_CLR_ABORT 0 // +113 bytes
- #define ENABLE_FEATURE_CHAT_SWALLOW_OPTS 0 // + 23 bytes
- */
- // default timeout: 45 sec
- #define DEFAULT_CHAT_TIMEOUT 45*1000
- // max length of "abort string",
- // i.e. device reply which causes termination
- #define MAX_ABORT_LEN 50
- // possible exit codes
- enum {
- ERR_OK = 0, // all's well
- ERR_MEM, // read too much while expecting
- ERR_IO, // signalled or I/O error
- ERR_TIMEOUT, // timed out while expecting
- ERR_ABORT, // first abort condition was met
- // ERR_ABORT2, // second abort condition was met
- // ...
- };
- // exit code
- // N.B> 10 bytes for volatile. Why all these signals?!
- static /*volatile*/ smallint exitcode;
- // trap for critical signals
- static void signal_handler(UNUSED_PARAM int signo)
- {
- // report I/O error condition
- exitcode = ERR_IO;
- }
- #if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
- #define unescape(s, nocr) unescape(s)
- #endif
- static size_t unescape(char *s, int *nocr)
- {
- char *start = s;
- char *p = s;
- while (*s) {
- char c = *s;
- // do we need special processing?
- // standard escapes + \s for space and \N for \0
- // \c inhibits terminating \r for commands and is noop for expects
- if ('\\' == c) {
- c = *++s;
- if (c) {
- #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
- if ('c' == c) {
- *nocr = 1;
- goto next;
- }
- #endif
- if ('N' == c) {
- c = '\0';
- } else if ('s' == c) {
- c = ' ';
- #if ENABLE_FEATURE_CHAT_NOFAIL
- // unescape leading dash only
- // TODO: and only for expect, not command string
- } else if ('-' == c && (start + 1 == s)) {
- //c = '-';
- #endif
- } else {
- c = bb_process_escape_sequence((const char **)&s);
- s--;
- }
- }
- // ^A becomes \001, ^B -- \002 and so on...
- } else if ('^' == c) {
- c = *++s-'@';
- }
- // put unescaped char
- *p++ = c;
- #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
- next:
- #endif
- // next char
- s++;
- }
- *p = '\0';
- return p - start;
- }
- int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
- int chat_main(int argc UNUSED_PARAM, char **argv)
- {
- // should we dump device output? to what fd? by default no.
- // this can be controlled later via ECHO {ON|OFF} chat directive
- // int echo_fd;
- bool echo = 0;
- // collection of device replies which cause unconditional termination
- llist_t *aborts = NULL;
- // inactivity period
- int timeout = DEFAULT_CHAT_TIMEOUT;
- // maximum length of abort string
- #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
- size_t max_abort_len = 0;
- #else
- #define max_abort_len MAX_ABORT_LEN
- #endif
- #if ENABLE_FEATURE_CHAT_TTY_HIFI
- struct termios tio0, tio;
- #endif
- // directive names
- enum {
- DIR_HANGUP = 0,
- DIR_ABORT,
- #if ENABLE_FEATURE_CHAT_CLR_ABORT
- DIR_CLR_ABORT,
- #endif
- DIR_TIMEOUT,
- DIR_ECHO,
- DIR_SAY,
- };
- // make x* functions fail with correct exitcode
- xfunc_error_retval = ERR_IO;
- // trap vanilla signals to prevent process from being killed suddenly
- bb_signals(0
- + (1 << SIGHUP)
- + (1 << SIGINT)
- + (1 << SIGTERM)
- + (1 << SIGPIPE)
- , signal_handler);
- #if ENABLE_FEATURE_CHAT_TTY_HIFI
- tcgetattr(STDIN_FILENO, &tio);
- tio0 = tio;
- cfmakeraw(&tio);
- tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
- #endif
- #if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
- getopt32(argv, "vVsSE");
- argv += optind;
- #else
- argv++; // goto first arg
- #endif
- // handle chat expect-send pairs
- while (*argv) {
- // directive given? process it
- int key = index_in_strings(
- "HANGUP\0" "ABORT\0"
- #if ENABLE_FEATURE_CHAT_CLR_ABORT
- "CLR_ABORT\0"
- #endif
- "TIMEOUT\0" "ECHO\0" "SAY\0"
- , *argv
- );
- if (key >= 0) {
- // cache directive value
- char *arg = *++argv;
- // ON -> 1, anything else -> 0
- bool onoff = !strcmp("ON", arg);
- // process directive
- if (DIR_HANGUP == key) {
- // turn SIGHUP on/off
- signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
- } else if (DIR_ABORT == key) {
- // append the string to abort conditions
- #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
- size_t len = strlen(arg);
- if (len > max_abort_len)
- max_abort_len = len;
- #endif
- llist_add_to_end(&aborts, arg);
- #if ENABLE_FEATURE_CHAT_CLR_ABORT
- } else if (DIR_CLR_ABORT == key) {
- // remove the string from abort conditions
- // N.B. gotta refresh maximum length too...
- #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
- max_abort_len = 0;
- #endif
- for (llist_t *l = aborts; l; l = l->link) {
- #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
- size_t len = strlen(l->data);
- #endif
- if (!strcmp(arg, l->data)) {
- llist_unlink(&aborts, l);
- continue;
- }
- #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
- if (len > max_abort_len)
- max_abort_len = len;
- #endif
- }
- #endif
- } else if (DIR_TIMEOUT == key) {
- // set new timeout
- // -1 means OFF
- timeout = atoi(arg) * 1000;
- // 0 means default
- // >0 means value in msecs
- if (!timeout)
- timeout = DEFAULT_CHAT_TIMEOUT;
- } else if (DIR_ECHO == key) {
- // turn echo on/off
- // N.B. echo means dumping output
- // from stdin (device) to stderr
- echo = onoff;
- //TODO? echo_fd = onoff * STDERR_FILENO;
- //TODO? echo_fd = xopen(arg, O_WRONLY|O_CREAT|O_TRUNC);
- } else if (DIR_SAY == key) {
- // just print argument verbatim
- fprintf(stderr, arg);
- }
- // next, please!
- argv++;
- // ordinary expect-send pair!
- } else {
- //-----------------------
- // do expect
- //-----------------------
- int expect_len;
- size_t buf_len = 0;
- size_t max_len = max_abort_len;
- struct pollfd pfd;
- #if ENABLE_FEATURE_CHAT_NOFAIL
- int nofail = 0;
- #endif
- char *expect = *argv++;
- // sanity check: shall we really expect something?
- if (!expect)
- goto expect_done;
- #if ENABLE_FEATURE_CHAT_NOFAIL
- // if expect starts with -
- if ('-' == *expect) {
- // swallow -
- expect++;
- // and enter nofail mode
- nofail++;
- }
- #endif
- #ifdef ___TEST___BUF___ // test behaviour with a small buffer
- # undef COMMON_BUFSIZE
- # define COMMON_BUFSIZE 6
- #endif
- // expand escape sequences in expect
- expect_len = unescape(expect, &expect_len /*dummy*/);
- if (expect_len > max_len)
- max_len = expect_len;
- // sanity check:
- // we should expect more than nothing but not more than input buffer
- // TODO: later we'll get rid of fixed-size buffer
- if (!expect_len)
- goto expect_done;
- if (max_len >= COMMON_BUFSIZE) {
- exitcode = ERR_MEM;
- goto expect_done;
- }
- // get reply
- pfd.fd = STDIN_FILENO;
- pfd.events = POLLIN;
- while (!exitcode
- && poll(&pfd, 1, timeout) > 0
- && (pfd.revents & POLLIN)
- ) {
- #define buf bb_common_bufsiz1
- llist_t *l;
- ssize_t delta;
- // read next char from device
- if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
- // dump device output if ECHO ON or RECORD fname
- //TODO? if (echo_fd > 0) {
- //TODO? full_write(echo_fd, buf+buf_len, 1);
- //TODO? }
- if (echo > 0)
- full_write(STDERR_FILENO, buf+buf_len, 1);
- buf_len++;
- // move input frame if we've reached higher bound
- if (buf_len > COMMON_BUFSIZE) {
- memmove(buf, buf+buf_len-max_len, max_len);
- buf_len = max_len;
- }
- }
- // N.B. rule of thumb: values being looked for can
- // be found only at the end of input buffer
- // this allows to get rid of strstr() and memmem()
- // TODO: make expect and abort strings processed uniformly
- // abort condition is met? -> bail out
- for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
- size_t len = strlen(l->data);
- delta = buf_len-len;
- if (delta >= 0 && !memcmp(buf+delta, l->data, len))
- goto expect_done;
- }
- exitcode = ERR_OK;
- // expected reply received? -> goto next command
- delta = buf_len - expect_len;
- if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
- goto expect_done;
- #undef buf
- }
- // device timed out or unexpected reply received
- exitcode = ERR_TIMEOUT;
- expect_done:
- #if ENABLE_FEATURE_CHAT_NOFAIL
- // on success and when in nofail mode
- // we should skip following subsend-subexpect pairs
- if (nofail) {
- if (!exitcode) {
- // find last send before non-dashed expect
- while (*argv && argv[1] && '-' == argv[1][0])
- argv += 2;
- // skip the pair
- // N.B. do we really need this?!
- if (!*argv++ || !*argv++)
- break;
- }
- // nofail mode also clears all but IO errors (or signals)
- if (ERR_IO != exitcode)
- exitcode = ERR_OK;
- }
- #endif
- // bail out unless we expected successfully
- if (exitcode)
- break;
- //-----------------------
- // do send
- //-----------------------
- if (*argv) {
- #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
- int nocr = 0; // inhibit terminating command with \r
- #endif
- char *loaded = NULL; // loaded command
- size_t len;
- char *buf = *argv++;
- // if command starts with @
- // load "real" command from file named after @
- if ('@' == *buf) {
- // skip the @ and any following white-space
- trim(++buf);
- buf = loaded = xmalloc_xopen_read_close(buf, NULL);
- }
- // expand escape sequences in command
- len = unescape(buf, &nocr);
- // send command
- #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
- pfd.fd = STDOUT_FILENO;
- pfd.events = POLLOUT;
- while (len && !exitcode
- && poll(&pfd, 1, timeout) > 0
- && (pfd.revents & POLLOUT)
- ) {
- // ugly! ugly! ugly!
- // gotta send char by char to achieve this!
- // Brrr...
- // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
- // "\\K" means send BREAK
- char c = *buf;
- if ('\\' == c) {
- c = *++buf;
- if ('d' == c) {
- sleep(1);
- len--;
- continue;
- } else if ('p' == c) {
- usleep(10000);
- len--;
- continue;
- } else if ('K' == c) {
- tcsendbreak(STDOUT_FILENO, 0);
- len--;
- continue;
- } else {
- buf--;
- }
- }
- if (safe_write(STDOUT_FILENO, buf, 1) > 0) {
- len--;
- buf++;
- } else
- break;
- }
- #else
- // if (len) {
- alarm(timeout);
- len -= full_write(STDOUT_FILENO, buf, len);
- alarm(0);
- // }
- #endif
- // report I/O error if there still exists at least one non-sent char
- if (len)
- exitcode = ERR_IO;
- // free loaded command (if any)
- if (loaded)
- free(loaded);
- #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
- // or terminate command with \r (if not inhibited)
- else if (!nocr)
- xwrite(STDOUT_FILENO, "\r", 1);
- #endif
- // bail out unless we sent command successfully
- if (exitcode)
- break;
- }
- }
- }
- #if ENABLE_FEATURE_CHAT_TTY_HIFI
- tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
- #endif
- return exitcode;
- }
|