123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- /* 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 source tree.
- */
- //config:config CHAT
- //config: bool "chat (6.7 kb)"
- //config: default y
- //config: help
- //config: Simple chat utility.
- //config:
- //config:config FEATURE_CHAT_NOFAIL
- //config: bool "Enable NOFAIL expect strings"
- //config: depends on CHAT
- //config: default y
- //config: help
- //config: When enabled expect strings which are started with a dash trigger
- //config: no-fail mode. That is when expectation is not met within timeout
- //config: the script is not terminated but sends next SEND string and waits
- //config: for next EXPECT string. This allows to compose far more flexible
- //config: scripts.
- //config:
- //config:config FEATURE_CHAT_TTY_HIFI
- //config: bool "Force STDIN to be a TTY"
- //config: depends on CHAT
- //config: default n
- //config: help
- //config: Original chat always treats STDIN as a TTY device and sets for it
- //config: so-called raw mode. This option turns on such behaviour.
- //config:
- //config:config FEATURE_CHAT_IMPLICIT_CR
- //config: bool "Enable implicit Carriage Return"
- //config: depends on CHAT
- //config: default y
- //config: help
- //config: When enabled make chat to terminate all SEND strings with a "\r"
- //config: unless "\c" is met anywhere in the string.
- //config:
- //config:config FEATURE_CHAT_SWALLOW_OPTS
- //config: bool "Swallow options"
- //config: depends on CHAT
- //config: default y
- //config: help
- //config: Busybox chat require no options. To make it not fail when used
- //config: in place of original chat (which has a bunch of options) turn
- //config: this on.
- //config:
- //config:config FEATURE_CHAT_SEND_ESCAPES
- //config: bool "Support weird SEND escapes"
- //config: depends on CHAT
- //config: default y
- //config: help
- //config: Original chat uses some escape sequences in SEND arguments which
- //config: are not sent to device but rather performs special actions.
- //config: E.g. "\K" means to send a break sequence to device.
- //config: "\d" delays execution for a second, "\p" -- for a 1/100 of second.
- //config: Before turning this option on think twice: do you really need them?
- //config:
- //config:config FEATURE_CHAT_VAR_ABORT_LEN
- //config: bool "Support variable-length ABORT conditions"
- //config: depends on CHAT
- //config: default y
- //config: help
- //config: Original chat uses fixed 50-bytes length ABORT conditions. Say N here.
- //config:
- //config:config FEATURE_CHAT_CLR_ABORT
- //config: bool "Support revoking of ABORT conditions"
- //config: depends on CHAT
- //config: default y
- //config: help
- //config: Support CLR_ABORT directive.
- //applet:IF_CHAT(APPLET(chat, BB_DIR_USR_SBIN, BB_SUID_DROP))
- //kbuild:lib-$(CONFIG_CHAT) += chat.o
- //usage:#define chat_trivial_usage
- //usage: "EXPECT [SEND [EXPECT [SEND]]...]"
- //usage:#define chat_full_usage "\n\n"
- //usage: "Useful for interacting with a modem connected to stdin/stdout.\n"
- //usage: "A script consists of \"expect-send\" argument pairs.\n"
- //usage: "Example:\n"
- //usage: "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"
- #include "libbb.h"
- #include "common_bufsiz.h"
- // 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
- #define exitcode bb_got_signal
- // 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)
- {
- int record_fd = -1;
- 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,
- DIR_RECORD,
- };
- #define inbuf bb_common_bufsiz1
- setup_common_bufsiz();
- // 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
- //TODO: use set_termios_to_raw()
- 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" "RECORD\0"
- , *argv
- );
- if (key >= 0) {
- bool onoff;
- // cache directive value
- char *arg = *++argv;
- if (!arg) {
- #if ENABLE_FEATURE_CHAT_TTY_HIFI
- tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
- #endif
- bb_show_usage();
- }
- // OFF -> 0, anything else -> 1
- onoff = (0 != strcmp("OFF", 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) {
- llist_t *l;
- // 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 (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) == 0) {
- 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 device input/output to stderr
- echo = onoff;
- } else if (DIR_RECORD == key) {
- // turn record on/off
- // N.B. record means dumping device input to a file
- // close previous record_fd
- if (record_fd > 0)
- close(record_fd);
- // N.B. do we have to die here on open error?
- record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
- } else if (DIR_SAY == key) {
- // just print argument verbatim
- // TODO: should we use full_write() to avoid unistd/stdio conflict?
- bb_simple_error_msg(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 == ERR_OK
- && poll(&pfd, 1, timeout) > 0
- /* && (pfd.revents & POLLIN) - may be untrue (e.g. only POLLERR set) */
- ) {
- llist_t *l;
- ssize_t delta;
- // read next char from device
- if (safe_read(STDIN_FILENO, inbuf + buf_len, 1) <= 0) {
- exitcode = ERR_IO;
- goto expect_done;
- }
- // dump device input if RECORD fname
- if (record_fd > 0) {
- full_write(record_fd, inbuf + buf_len, 1);
- }
- // dump device input if ECHO ON
- if (echo) {
- // if (inbuf[buf_len] < ' ') {
- // full_write2_str("^");
- // inbuf[buf_len] += '@';
- // }
- full_write(STDERR_FILENO, inbuf + buf_len, 1);
- }
- buf_len++;
- // move input frame if we've reached higher bound
- if (buf_len > COMMON_BUFSIZE) {
- memmove(inbuf, inbuf + 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(inbuf + 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(inbuf + delta, expect, expect_len) == 0)
- goto expect_done;
- } /* while (have data) */
- // device timed out, or unexpected reply received,
- // or we got a signal (poll() returned -1 with EINTR).
- 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 != ERR_OK)
- 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
- alarm(timeout);
- pfd.fd = STDOUT_FILENO;
- pfd.events = POLLOUT;
- while (len && !exitcode
- && poll(&pfd, 1, -1) > 0
- && (pfd.revents & POLLOUT)
- ) {
- #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
- // "\\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) {
- sleep1();
- len--;
- continue;
- }
- if ('p' == c) {
- msleep(10);
- len--;
- continue;
- }
- if ('K' == c) {
- tcsendbreak(STDOUT_FILENO, 0);
- len--;
- continue;
- }
- buf--;
- }
- if (safe_write(STDOUT_FILENO, buf, 1) != 1)
- break;
- len--;
- buf++;
- #else
- len -= full_write(STDOUT_FILENO, buf, len);
- #endif
- } /* while (can write) */
- alarm(0);
- // 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_str(STDOUT_FILENO, "\r");
- #endif
- // bail out unless we sent command successfully
- if (exitcode)
- break;
- } /* if (*argv) */
- }
- } /* while (*argv) */
- #if ENABLE_FEATURE_CHAT_TTY_HIFI
- tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
- #endif
- return exitcode;
- }
|