busybox/miscutils/chat.c

527 lines
14 KiB
C
Raw Normal View History

2008-02-19 17:05:08 +05:30
/* 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.
2008-02-19 17:05:08 +05:30
*/
//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 '~'"
2008-02-19 17:05:08 +05:30
#include "libbb.h"
#include "common_bufsiz.h"
2008-02-19 17:05:08 +05:30
// default timeout: 45 sec
#define DEFAULT_CHAT_TIMEOUT 45*1000
2008-02-19 17:05:08 +05:30
// 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
2008-02-19 17:05:08 +05:30
// trap for critical signals
2008-07-05 14:48:54 +05:30
static void signal_handler(UNUSED_PARAM int signo)
2008-02-19 17:05:08 +05:30
{
// 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;
2008-07-05 14:48:54 +05:30
int chat_main(int argc UNUSED_PARAM, char **argv)
2008-02-19 17:05:08 +05:30
{
int record_fd = -1;
2008-02-19 17:05:08 +05:30
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,
2008-02-19 17:05:08 +05:30
};
// 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()
2008-02-19 17:05:08 +05:30
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"
2008-02-19 17:05:08 +05:30
, *argv
);
if (key >= 0) {
bool onoff;
2008-02-19 17:05:08 +05:30
// 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));
2008-02-19 17:05:08 +05:30
// 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;
2008-02-19 17:05:08 +05:30
// remove the string from abort conditions
// N.B. gotta refresh maximum length too...
# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
2008-02-19 17:05:08 +05:30
max_abort_len = 0;
# endif
for (l = aborts; l; l = l->link) {
# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
2008-02-19 17:05:08 +05:30
size_t len = strlen(l->data);
# endif
if (strcmp(arg, l->data) == 0) {
2008-02-19 17:05:08 +05:30
llist_unlink(&aborts, l);
continue;
}
# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
2008-02-19 17:05:08 +05:30
if (len > max_abort_len)
max_abort_len = len;
# endif
2008-02-19 17:05:08 +05:30
}
#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
2008-02-19 17:05:08 +05:30
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;
2008-02-19 17:05:08 +05:30
} else if (DIR_SAY == key) {
// just print argument verbatim
// TODO: should we use full_write() to avoid unistd/stdio conflict?
libbb: reduce the overhead of single parameter bb_error_msg() calls Back in 2007, commit 0c97c9d43707 ("'simple' error message functions by Loic Grenie") introduced bb_simple_perror_msg() to allow for a lower overhead call to bb_perror_msg() when only a string was being printed with no parameters. This saves space for some CPU architectures because it avoids the overhead of a call to a variadic function. However there has never been a simple version of bb_error_msg(), and since 2007 many new calls to bb_perror_msg() have been added that only take a single parameter and so could have been using bb_simple_perror_message(). This changeset introduces 'simple' versions of bb_info_msg(), bb_error_msg(), bb_error_msg_and_die(), bb_herror_msg() and bb_herror_msg_and_die(), and replaces all calls that only take a single parameter, or use something like ("%s", arg), with calls to the corresponding 'simple' version. Since it is likely that single parameter calls to the variadic functions may be accidentally reintroduced in the future a new debugging config option WARN_SIMPLE_MSG has been introduced. This uses some macro magic which will cause any such calls to generate a warning, but this is turned off by default to avoid use of the unpleasant macros in normal circumstances. This is a large changeset due to the number of calls that have been replaced. The only files that contain changes other than simple substitution of function calls are libbb.h, libbb/herror_msg.c, libbb/verror_msg.c and libbb/xfuncs_printf.c. In miscutils/devfsd.c, networking/udhcp/common.h and util-linux/mdev.c additonal macros have been added for logging so that single parameter and multiple parameter logging variants exist. The amount of space saved varies considerably by architecture, and was found to be as follows (for 'defconfig' using GCC 7.4): Arm: -92 bytes MIPS: -52 bytes PPC: -1836 bytes x86_64: -938 bytes Note that for the MIPS architecture only an exception had to be made disabling the 'simple' calls for 'udhcp' (in networking/udhcp/common.h) because it made these files larger on MIPS. Signed-off-by: James Byrne <james.byrne@origamienergy.com> Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
2019-07-02 15:05:03 +05:30
bb_simple_error_msg(arg);
2008-02-19 17:05:08 +05:30
}
// next, please!
argv++;
// ordinary expect-send pair!
} else {
//-----------------------
// do expect
//-----------------------
int expect_len;
size_t buf_len = 0;
2008-02-19 17:05:08 +05:30
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();
2008-02-19 17:05:08 +05:30
// 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] += '@';
// }
2008-02-19 17:05:08 +05:30
full_write(STDERR_FILENO, buf+buf_len, 1);
}
2008-02-19 17:05:08 +05:30
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;
2008-02-19 17:05:08 +05:30
if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
goto expect_done;
#undef buf
} /* while (have data) */
2008-02-19 17:05:08 +05:30
// 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);
2008-02-19 17:05:08 +05:30
}
// expand escape sequences in command
len = unescape(buf, &nocr);
// send command
alarm(timeout);
2008-02-19 17:05:08 +05:30
pfd.fd = STDOUT_FILENO;
pfd.events = POLLOUT;
while (len && !exitcode
&& poll(&pfd, 1, -1) > 0
2008-02-19 17:05:08 +05:30
&& (pfd.revents & POLLOUT)
) {
#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
2008-02-19 17:05:08 +05:30
// "\\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) {
2008-02-19 17:05:08 +05:30
usleep(10000);
len--;
continue;
}
if ('K' == c) {
2008-02-19 17:05:08 +05:30
tcsendbreak(STDOUT_FILENO, 0);
len--;
continue;
}
buf--;
2008-02-19 17:05:08 +05:30
}
if (safe_write(STDOUT_FILENO, buf, 1) != 1)
2008-02-19 17:05:08 +05:30
break;
len--;
buf++;
2008-02-19 17:05:08 +05:30
#else
len -= full_write(STDOUT_FILENO, buf, len);
#endif
} /* while (can write) */
alarm(0);
2008-02-19 17:05:08 +05:30
// 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) */
2008-02-19 17:05:08 +05:30
}
} /* while (*argv) */
2008-02-19 17:05:08 +05:30
#if ENABLE_FEATURE_CHAT_TTY_HIFI
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
#endif
return exitcode;
}