blob: a04565063d207552d5c6376ea0d60553df2cd72f [file] [log] [blame]
/* 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.3 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,
};
// 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
&& poll(&pfd, 1, timeout) > 0
&& (pfd.revents & POLLIN)
) {
llist_t *l;
ssize_t delta;
#define buf bb_common_bufsiz1
setup_common_bufsiz();
// read next char from device
if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
// dump device input if RECORD fname
if (record_fd > 0) {
full_write(record_fd, buf+buf_len, 1);
}
// dump device input if ECHO ON
if (echo) {
// if (buf[buf_len] < ' ') {
// full_write(STDERR_FILENO, "^", 1);
// buf[buf_len] += '@';
// }
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
} /* while (have data) */
// 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
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) {
sleep(1);
len--;
continue;
}
if ('p' == c) {
usleep(10000);
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(STDOUT_FILENO, "\r", 1);
#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;
}