diff options
Diffstat (limited to 'miscutils')
-rw-r--r-- | miscutils/chat.c | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/miscutils/chat.c b/miscutils/chat.c new file mode 100644 index 0000000..4f55738 --- /dev/null +++ b/miscutils/chat.c @@ -0,0 +1,443 @@ +/* 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(ATTRIBUTE_UNUSED 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, 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 + //----------------------- + size_t expect_len, 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_open_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; +} |