1 /* vi: set sw=4 ts=4: */
3 * bare bones chat utility
4 * inspired by ppp's chat
6 * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
8 * Licensed under GPLv2, see file LICENSE in this source tree.
11 //config: bool "chat (6.7 kb)"
14 //config: Simple chat utility.
16 //config:config FEATURE_CHAT_NOFAIL
17 //config: bool "Enable NOFAIL expect strings"
18 //config: depends on CHAT
21 //config: When enabled expect strings which are started with a dash trigger
22 //config: no-fail mode. That is when expectation is not met within timeout
23 //config: the script is not terminated but sends next SEND string and waits
24 //config: for next EXPECT string. This allows to compose far more flexible
27 //config:config FEATURE_CHAT_TTY_HIFI
28 //config: bool "Force STDIN to be a TTY"
29 //config: depends on CHAT
32 //config: Original chat always treats STDIN as a TTY device and sets for it
33 //config: so-called raw mode. This option turns on such behaviour.
35 //config:config FEATURE_CHAT_IMPLICIT_CR
36 //config: bool "Enable implicit Carriage Return"
37 //config: depends on CHAT
40 //config: When enabled make chat to terminate all SEND strings with a "\r"
41 //config: unless "\c" is met anywhere in the string.
43 //config:config FEATURE_CHAT_SWALLOW_OPTS
44 //config: bool "Swallow options"
45 //config: depends on CHAT
48 //config: Busybox chat require no options. To make it not fail when used
49 //config: in place of original chat (which has a bunch of options) turn
52 //config:config FEATURE_CHAT_SEND_ESCAPES
53 //config: bool "Support weird SEND escapes"
54 //config: depends on CHAT
57 //config: Original chat uses some escape sequences in SEND arguments which
58 //config: are not sent to device but rather performs special actions.
59 //config: E.g. "\K" means to send a break sequence to device.
60 //config: "\d" delays execution for a second, "\p" -- for a 1/100 of second.
61 //config: Before turning this option on think twice: do you really need them?
63 //config:config FEATURE_CHAT_VAR_ABORT_LEN
64 //config: bool "Support variable-length ABORT conditions"
65 //config: depends on CHAT
68 //config: Original chat uses fixed 50-bytes length ABORT conditions. Say N here.
70 //config:config FEATURE_CHAT_CLR_ABORT
71 //config: bool "Support revoking of ABORT conditions"
72 //config: depends on CHAT
75 //config: Support CLR_ABORT directive.
77 //applet:IF_CHAT(APPLET(chat, BB_DIR_USR_SBIN, BB_SUID_DROP))
79 //kbuild:lib-$(CONFIG_CHAT) += chat.o
81 //usage:#define chat_trivial_usage
82 //usage: "EXPECT [SEND [EXPECT [SEND]]...]"
83 //usage:#define chat_full_usage "\n\n"
84 //usage: "Useful for interacting with a modem connected to stdin/stdout.\n"
85 //usage: "A script consists of \"expect-send\" argument pairs.\n"
87 //usage: "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"
90 #include "common_bufsiz.h"
92 // default timeout: 45 sec
93 #define DEFAULT_CHAT_TIMEOUT 45*1000
94 // max length of "abort string",
95 // i.e. device reply which causes termination
96 #define MAX_ABORT_LEN 50
98 // possible exit codes
100 ERR_OK
= 0, // all's well
101 ERR_MEM
, // read too much while expecting
102 ERR_IO
, // signalled or I/O error
103 ERR_TIMEOUT
, // timed out while expecting
104 ERR_ABORT
, // first abort condition was met
105 // ERR_ABORT2, // second abort condition was met
110 #define exitcode bb_got_signal
112 // trap for critical signals
113 static void signal_handler(UNUSED_PARAM
int signo
)
115 // report I/O error condition
119 #if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
120 #define unescape(s, nocr) unescape(s)
122 static size_t unescape(char *s
, int *nocr
)
129 // do we need special processing?
130 // standard escapes + \s for space and \N for \0
131 // \c inhibits terminating \r for commands and is noop for expects
135 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
143 } else if ('s' == c
) {
145 #if ENABLE_FEATURE_CHAT_NOFAIL
146 // unescape leading dash only
147 // TODO: and only for expect, not command string
148 } else if ('-' == c
&& (start
+ 1 == s
)) {
152 c
= bb_process_escape_sequence((const char **)&s
);
156 // ^A becomes \001, ^B -- \002 and so on...
157 } else if ('^' == c
) {
160 // put unescaped char
162 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
173 int chat_main(int argc
, char **argv
) MAIN_EXTERNALLY_VISIBLE
;
174 int chat_main(int argc UNUSED_PARAM
, char **argv
)
178 // collection of device replies which cause unconditional termination
179 llist_t
*aborts
= NULL
;
181 int timeout
= DEFAULT_CHAT_TIMEOUT
;
182 // maximum length of abort string
183 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
184 size_t max_abort_len
= 0;
186 #define max_abort_len MAX_ABORT_LEN
188 #if ENABLE_FEATURE_CHAT_TTY_HIFI
189 struct termios tio0
, tio
;
195 #if ENABLE_FEATURE_CHAT_CLR_ABORT
204 #define inbuf bb_common_bufsiz1
205 setup_common_bufsiz();
207 // make x* functions fail with correct exitcode
208 xfunc_error_retval
= ERR_IO
;
210 // trap vanilla signals to prevent process from being killed suddenly
218 #if ENABLE_FEATURE_CHAT_TTY_HIFI
219 //TODO: use set_termios_to_raw()
220 tcgetattr(STDIN_FILENO
, &tio
);
223 tcsetattr(STDIN_FILENO
, TCSAFLUSH
, &tio
);
226 #if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
227 getopt32(argv
, "vVsSE");
230 argv
++; // goto first arg
232 // handle chat expect-send pairs
234 // directive given? process it
235 int key
= index_in_strings(
237 #if ENABLE_FEATURE_CHAT_CLR_ABORT
240 "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
245 // cache directive value
249 #if ENABLE_FEATURE_CHAT_TTY_HIFI
250 tcsetattr(STDIN_FILENO
, TCSAFLUSH
, &tio0
);
254 // OFF -> 0, anything else -> 1
255 onoff
= (0 != strcmp("OFF", arg
));
257 if (DIR_HANGUP
== key
) {
258 // turn SIGHUP on/off
259 signal(SIGHUP
, onoff
? signal_handler
: SIG_IGN
);
260 } else if (DIR_ABORT
== key
) {
261 // append the string to abort conditions
262 #if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
263 size_t len
= strlen(arg
);
264 if (len
> max_abort_len
)
267 llist_add_to_end(&aborts
, arg
);
268 #if ENABLE_FEATURE_CHAT_CLR_ABORT
269 } else if (DIR_CLR_ABORT
== key
) {
271 // remove the string from abort conditions
272 // N.B. gotta refresh maximum length too...
273 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
276 for (l
= aborts
; l
; l
= l
->link
) {
277 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
278 size_t len
= strlen(l
->data
);
280 if (strcmp(arg
, l
->data
) == 0) {
281 llist_unlink(&aborts
, l
);
284 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
285 if (len
> max_abort_len
)
290 } else if (DIR_TIMEOUT
== key
) {
293 timeout
= atoi(arg
) * 1000;
295 // >0 means value in msecs
297 timeout
= DEFAULT_CHAT_TIMEOUT
;
298 } else if (DIR_ECHO
== key
) {
300 // N.B. echo means dumping device input/output to stderr
302 } else if (DIR_RECORD
== key
) {
303 // turn record on/off
304 // N.B. record means dumping device input to a file
305 // close previous record_fd
308 // N.B. do we have to die here on open error?
309 record_fd
= (onoff
) ? xopen(arg
, O_WRONLY
|O_CREAT
|O_TRUNC
) : -1;
310 } else if (DIR_SAY
== key
) {
311 // just print argument verbatim
312 // TODO: should we use full_write() to avoid unistd/stdio conflict?
313 bb_simple_error_msg(arg
);
317 // ordinary expect-send pair!
319 //-----------------------
321 //-----------------------
324 size_t max_len
= max_abort_len
;
327 #if ENABLE_FEATURE_CHAT_NOFAIL
330 char *expect
= *argv
++;
332 // sanity check: shall we really expect something?
336 #if ENABLE_FEATURE_CHAT_NOFAIL
337 // if expect starts with -
338 if ('-' == *expect
) {
341 // and enter nofail mode
346 #ifdef ___TEST___BUF___ // test behaviour with a small buffer
347 # undef COMMON_BUFSIZE
348 # define COMMON_BUFSIZE 6
350 // expand escape sequences in expect
351 expect_len
= unescape(expect
, &expect_len
/*dummy*/);
352 if (expect_len
> max_len
)
353 max_len
= expect_len
;
355 // we should expect more than nothing but not more than input buffer
356 // TODO: later we'll get rid of fixed-size buffer
359 if (max_len
>= COMMON_BUFSIZE
) {
365 pfd
.fd
= STDIN_FILENO
;
367 while (exitcode
== ERR_OK
368 && poll(&pfd
, 1, timeout
) > 0
369 /* && (pfd.revents & POLLIN) - may be untrue (e.g. only POLLERR set) */
374 // read next char from device
375 if (safe_read(STDIN_FILENO
, inbuf
+ buf_len
, 1) <= 0) {
380 // dump device input if RECORD fname
382 full_write(record_fd
, inbuf
+ buf_len
, 1);
384 // dump device input if ECHO ON
386 // if (inbuf[buf_len] < ' ') {
387 // full_write2_str("^");
388 // inbuf[buf_len] += '@';
390 full_write(STDERR_FILENO
, inbuf
+ buf_len
, 1);
393 // move input frame if we've reached higher bound
394 if (buf_len
> COMMON_BUFSIZE
) {
395 memmove(inbuf
, inbuf
+ buf_len
- max_len
, max_len
);
398 // N.B. rule of thumb: values being looked for can
399 // be found only at the end of input buffer
400 // this allows to get rid of strstr() and memmem()
402 // TODO: make expect and abort strings processed uniformly
403 // abort condition is met? -> bail out
404 for (l
= aborts
, exitcode
= ERR_ABORT
; l
; l
= l
->link
, ++exitcode
) {
405 size_t len
= strlen(l
->data
);
406 delta
= buf_len
- len
;
407 if (delta
>= 0 && !memcmp(inbuf
+ delta
, l
->data
, len
))
412 // expected reply received? -> goto next command
413 delta
= buf_len
- expect_len
;
414 if (delta
>= 0 && memcmp(inbuf
+ delta
, expect
, expect_len
) == 0)
416 } /* while (have data) */
418 // device timed out, or unexpected reply received,
419 // or we got a signal (poll() returned -1 with EINTR).
420 exitcode
= ERR_TIMEOUT
;
422 #if ENABLE_FEATURE_CHAT_NOFAIL
423 // on success and when in nofail mode
424 // we should skip following subsend-subexpect pairs
427 // find last send before non-dashed expect
428 while (*argv
&& argv
[1] && '-' == argv
[1][0])
431 // N.B. do we really need this?!
432 if (!*argv
++ || !*argv
++)
435 // nofail mode also clears all but IO errors (or signals)
436 if (ERR_IO
!= exitcode
)
440 // bail out unless we expected successfully
441 if (exitcode
!= ERR_OK
)
444 //-----------------------
446 //-----------------------
448 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
449 int nocr
= 0; // inhibit terminating command with \r
451 char *loaded
= NULL
; // loaded command
455 // if command starts with @
456 // load "real" command from file named after @
458 // skip the @ and any following white-space
460 buf
= loaded
= xmalloc_xopen_read_close(buf
, NULL
);
462 // expand escape sequences in command
463 len
= unescape(buf
, &nocr
);
467 pfd
.fd
= STDOUT_FILENO
;
468 pfd
.events
= POLLOUT
;
469 while (len
&& !exitcode
470 && poll(&pfd
, 1, -1) > 0
471 && (pfd
.revents
& POLLOUT
)
473 #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
474 // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
475 // "\\K" means send BREAK
490 tcsendbreak(STDOUT_FILENO
, 0);
496 if (safe_write(STDOUT_FILENO
, buf
, 1) != 1)
501 len
-= full_write(STDOUT_FILENO
, buf
, len
);
503 } /* while (can write) */
506 // report I/O error if there still exists at least one non-sent char
510 // free loaded command (if any)
513 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
514 // or terminate command with \r (if not inhibited)
516 xwrite_str(STDOUT_FILENO
, "\r");
518 // bail out unless we sent command successfully
523 } /* while (*argv) */
525 #if ENABLE_FEATURE_CHAT_TTY_HIFI
526 tcsetattr(STDIN_FILENO
, TCSAFLUSH
, &tio0
);