6937487be7
Back in 2007, commit 0c97c9d437
("'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>
527 lines
14 KiB
C
527 lines
14 KiB
C
/* 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;
|
|
}
|