chat: now with "svn add"
No binary (build failed)
—
(log yok)
| @@ -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 <[email protected]> |
| + | * |
| + | * 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; |
| + | } |