function old new delta chat_main 1295 1303 +8 Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
		
			
				
	
	
		
			531 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			531 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,
 | 
						|
	};
 | 
						|
 | 
						|
#define inbuf bb_common_bufsiz1
 | 
						|
	setup_common_bufsiz();
 | 
						|
 | 
						|
	// 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 == ERR_OK
 | 
						|
			    && poll(&pfd, 1, timeout) > 0
 | 
						|
			    /* && (pfd.revents & POLLIN) - may be untrue (e.g. only POLLERR set) */
 | 
						|
			) {
 | 
						|
				llist_t *l;
 | 
						|
				ssize_t delta;
 | 
						|
 | 
						|
				// read next char from device
 | 
						|
				if (safe_read(STDIN_FILENO, inbuf + buf_len, 1) <= 0) {
 | 
						|
					exitcode = ERR_IO;
 | 
						|
					goto expect_done;
 | 
						|
				}
 | 
						|
 | 
						|
				// dump device input if RECORD fname
 | 
						|
				if (record_fd > 0) {
 | 
						|
					full_write(record_fd, inbuf + buf_len, 1);
 | 
						|
				}
 | 
						|
				// dump device input if ECHO ON
 | 
						|
				if (echo) {
 | 
						|
//					if (inbuf[buf_len] < ' ') {
 | 
						|
//						full_write2_str("^");
 | 
						|
//						inbuf[buf_len] += '@';
 | 
						|
//					}
 | 
						|
					full_write(STDERR_FILENO, inbuf + buf_len, 1);
 | 
						|
				}
 | 
						|
				buf_len++;
 | 
						|
				// move input frame if we've reached higher bound
 | 
						|
				if (buf_len > COMMON_BUFSIZE) {
 | 
						|
					memmove(inbuf, inbuf + 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(inbuf + 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(inbuf + delta, expect, expect_len) == 0)
 | 
						|
					goto expect_done;
 | 
						|
			} /* while (have data) */
 | 
						|
 | 
						|
			// device timed out, or unexpected reply received,
 | 
						|
			// or we got a signal (poll() returned -1 with EINTR).
 | 
						|
			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 != ERR_OK)
 | 
						|
				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) {
 | 
						|
							sleep1();
 | 
						|
							len--;
 | 
						|
							continue;
 | 
						|
						}
 | 
						|
						if ('p' == c) {
 | 
						|
							msleep(10);
 | 
						|
							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_str(STDOUT_FILENO, "\r");
 | 
						|
#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;
 | 
						|
}
 |