summaryrefslogtreecommitdiff
path: root/miscutils/chat.c (plain)
blob: dc85f82fbaab219cdd556237045b69f46b28d18e
1/* vi: set sw=4 ts=4: */
2/*
3 * bare bones chat utility
4 * inspired by ppp's chat
5 *
6 * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
7 *
8 * Licensed under GPLv2, see file LICENSE in this source tree.
9 */
10//config:config CHAT
11//config: bool "chat"
12//config: default y
13//config: help
14//config: Simple chat utility.
15//config:
16//config:config FEATURE_CHAT_NOFAIL
17//config: bool "Enable NOFAIL expect strings"
18//config: depends on CHAT
19//config: default y
20//config: help
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
25//config: scripts.
26//config:
27//config:config FEATURE_CHAT_TTY_HIFI
28//config: bool "Force STDIN to be a TTY"
29//config: depends on CHAT
30//config: default n
31//config: help
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.
34//config:
35//config:config FEATURE_CHAT_IMPLICIT_CR
36//config: bool "Enable implicit Carriage Return"
37//config: depends on CHAT
38//config: default y
39//config: help
40//config: When enabled make chat to terminate all SEND strings with a "\r"
41//config: unless "\c" is met anywhere in the string.
42//config:
43//config:config FEATURE_CHAT_SWALLOW_OPTS
44//config: bool "Swallow options"
45//config: depends on CHAT
46//config: default y
47//config: help
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
50//config: this on.
51//config:
52//config:config FEATURE_CHAT_SEND_ESCAPES
53//config: bool "Support weird SEND escapes"
54//config: depends on CHAT
55//config: default y
56//config: help
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?
62//config:
63//config:config FEATURE_CHAT_VAR_ABORT_LEN
64//config: bool "Support variable-length ABORT conditions"
65//config: depends on CHAT
66//config: default y
67//config: help
68//config: Original chat uses fixed 50-bytes length ABORT conditions. Say N here.
69//config:
70//config:config FEATURE_CHAT_CLR_ABORT
71//config: bool "Support revoking of ABORT conditions"
72//config: depends on CHAT
73//config: default y
74//config: help
75//config: Support CLR_ABORT directive.
76
77//applet:IF_CHAT(APPLET(chat, BB_DIR_USR_SBIN, BB_SUID_DROP))
78
79//kbuild:lib-$(CONFIG_CHAT) += chat.o
80
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 one or more \"expect-send\" pairs of strings,\n"
86//usage: "each pair is a pair of arguments. Example:\n"
87//usage: "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"
88
89#include "libbb.h"
90#include "common_bufsiz.h"
91
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
97
98// possible exit codes
99enum {
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
106// ...
107};
108
109// exit code
110#define exitcode bb_got_signal
111
112// trap for critical signals
113static void signal_handler(UNUSED_PARAM int signo)
114{
115 // report I/O error condition
116 exitcode = ERR_IO;
117}
118
119#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
120#define unescape(s, nocr) unescape(s)
121#endif
122static size_t unescape(char *s, int *nocr)
123{
124 char *start = s;
125 char *p = s;
126
127 while (*s) {
128 char c = *s;
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
132 if ('\\' == c) {
133 c = *++s;
134 if (c) {
135#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
136 if ('c' == c) {
137 *nocr = 1;
138 goto next;
139 }
140#endif
141 if ('N' == c) {
142 c = '\0';
143 } else if ('s' == c) {
144 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)) {
149 //c = '-';
150#endif
151 } else {
152 c = bb_process_escape_sequence((const char **)&s);
153 s--;
154 }
155 }
156 // ^A becomes \001, ^B -- \002 and so on...
157 } else if ('^' == c) {
158 c = *++s-'@';
159 }
160 // put unescaped char
161 *p++ = c;
162#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
163 next:
164#endif
165 // next char
166 s++;
167 }
168 *p = '\0';
169
170 return p - start;
171}
172
173int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
174int chat_main(int argc UNUSED_PARAM, char **argv)
175{
176 int record_fd = -1;
177 bool echo = 0;
178 // collection of device replies which cause unconditional termination
179 llist_t *aborts = NULL;
180 // inactivity period
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;
185#else
186#define max_abort_len MAX_ABORT_LEN
187#endif
188#if ENABLE_FEATURE_CHAT_TTY_HIFI
189 struct termios tio0, tio;
190#endif
191 // directive names
192 enum {
193 DIR_HANGUP = 0,
194 DIR_ABORT,
195#if ENABLE_FEATURE_CHAT_CLR_ABORT
196 DIR_CLR_ABORT,
197#endif
198 DIR_TIMEOUT,
199 DIR_ECHO,
200 DIR_SAY,
201 DIR_RECORD,
202 };
203
204 // make x* functions fail with correct exitcode
205 xfunc_error_retval = ERR_IO;
206
207 // trap vanilla signals to prevent process from being killed suddenly
208 bb_signals(0
209 + (1 << SIGHUP)
210 + (1 << SIGINT)
211 + (1 << SIGTERM)
212 + (1 << SIGPIPE)
213 , signal_handler);
214
215#if ENABLE_FEATURE_CHAT_TTY_HIFI
216 tcgetattr(STDIN_FILENO, &tio);
217 tio0 = tio;
218 cfmakeraw(&tio);
219 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
220#endif
221
222#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
223 getopt32(argv, "vVsSE");
224 argv += optind;
225#else
226 argv++; // goto first arg
227#endif
228 // handle chat expect-send pairs
229 while (*argv) {
230 // directive given? process it
231 int key = index_in_strings(
232 "HANGUP\0" "ABORT\0"
233#if ENABLE_FEATURE_CHAT_CLR_ABORT
234 "CLR_ABORT\0"
235#endif
236 "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
237 , *argv
238 );
239 if (key >= 0) {
240 // cache directive value
241 char *arg = *++argv;
242 // OFF -> 0, anything else -> 1
243 bool onoff = (0 != strcmp("OFF", arg));
244 // process directive
245 if (DIR_HANGUP == key) {
246 // turn SIGHUP on/off
247 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
248 } else if (DIR_ABORT == key) {
249 // append the string to abort conditions
250#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
251 size_t len = strlen(arg);
252 if (len > max_abort_len)
253 max_abort_len = len;
254#endif
255 llist_add_to_end(&aborts, arg);
256#if ENABLE_FEATURE_CHAT_CLR_ABORT
257 } else if (DIR_CLR_ABORT == key) {
258 llist_t *l;
259 // remove the string from abort conditions
260 // N.B. gotta refresh maximum length too...
261# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
262 max_abort_len = 0;
263# endif
264 for (l = aborts; l; l = l->link) {
265# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
266 size_t len = strlen(l->data);
267# endif
268 if (strcmp(arg, l->data) == 0) {
269 llist_unlink(&aborts, l);
270 continue;
271 }
272# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
273 if (len > max_abort_len)
274 max_abort_len = len;
275# endif
276 }
277#endif
278 } else if (DIR_TIMEOUT == key) {
279 // set new timeout
280 // -1 means OFF
281 timeout = atoi(arg) * 1000;
282 // 0 means default
283 // >0 means value in msecs
284 if (!timeout)
285 timeout = DEFAULT_CHAT_TIMEOUT;
286 } else if (DIR_ECHO == key) {
287 // turn echo on/off
288 // N.B. echo means dumping device input/output to stderr
289 echo = onoff;
290 } else if (DIR_RECORD == key) {
291 // turn record on/off
292 // N.B. record means dumping device input to a file
293 // close previous record_fd
294 if (record_fd > 0)
295 close(record_fd);
296 // N.B. do we have to die here on open error?
297 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
298 } else if (DIR_SAY == key) {
299 // just print argument verbatim
300 // TODO: should we use full_write() to avoid unistd/stdio conflict?
301 bb_error_msg("%s", arg);
302 }
303 // next, please!
304 argv++;
305 // ordinary expect-send pair!
306 } else {
307 //-----------------------
308 // do expect
309 //-----------------------
310 int expect_len;
311 size_t buf_len = 0;
312 size_t max_len = max_abort_len;
313
314 struct pollfd pfd;
315#if ENABLE_FEATURE_CHAT_NOFAIL
316 int nofail = 0;
317#endif
318 char *expect = *argv++;
319
320 // sanity check: shall we really expect something?
321 if (!expect)
322 goto expect_done;
323
324#if ENABLE_FEATURE_CHAT_NOFAIL
325 // if expect starts with -
326 if ('-' == *expect) {
327 // swallow -
328 expect++;
329 // and enter nofail mode
330 nofail++;
331 }
332#endif
333
334#ifdef ___TEST___BUF___ // test behaviour with a small buffer
335# undef COMMON_BUFSIZE
336# define COMMON_BUFSIZE 6
337#endif
338 // expand escape sequences in expect
339 expect_len = unescape(expect, &expect_len /*dummy*/);
340 if (expect_len > max_len)
341 max_len = expect_len;
342 // sanity check:
343 // we should expect more than nothing but not more than input buffer
344 // TODO: later we'll get rid of fixed-size buffer
345 if (!expect_len)
346 goto expect_done;
347 if (max_len >= COMMON_BUFSIZE) {
348 exitcode = ERR_MEM;
349 goto expect_done;
350 }
351
352 // get reply
353 pfd.fd = STDIN_FILENO;
354 pfd.events = POLLIN;
355 while (!exitcode
356 && poll(&pfd, 1, timeout) > 0
357 && (pfd.revents & POLLIN)
358 ) {
359 llist_t *l;
360 ssize_t delta;
361#define buf bb_common_bufsiz1
362 setup_common_bufsiz();
363
364 // read next char from device
365 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
366 // dump device input if RECORD fname
367 if (record_fd > 0) {
368 full_write(record_fd, buf+buf_len, 1);
369 }
370 // dump device input if ECHO ON
371 if (echo) {
372// if (buf[buf_len] < ' ') {
373// full_write(STDERR_FILENO, "^", 1);
374// buf[buf_len] += '@';
375// }
376 full_write(STDERR_FILENO, buf+buf_len, 1);
377 }
378 buf_len++;
379 // move input frame if we've reached higher bound
380 if (buf_len > COMMON_BUFSIZE) {
381 memmove(buf, buf+buf_len-max_len, max_len);
382 buf_len = max_len;
383 }
384 }
385 // N.B. rule of thumb: values being looked for can
386 // be found only at the end of input buffer
387 // this allows to get rid of strstr() and memmem()
388
389 // TODO: make expect and abort strings processed uniformly
390 // abort condition is met? -> bail out
391 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
392 size_t len = strlen(l->data);
393 delta = buf_len-len;
394 if (delta >= 0 && !memcmp(buf+delta, l->data, len))
395 goto expect_done;
396 }
397 exitcode = ERR_OK;
398
399 // expected reply received? -> goto next command
400 delta = buf_len - expect_len;
401 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
402 goto expect_done;
403#undef buf
404 } /* while (have data) */
405
406 // device timed out or unexpected reply received
407 exitcode = ERR_TIMEOUT;
408 expect_done:
409#if ENABLE_FEATURE_CHAT_NOFAIL
410 // on success and when in nofail mode
411 // we should skip following subsend-subexpect pairs
412 if (nofail) {
413 if (!exitcode) {
414 // find last send before non-dashed expect
415 while (*argv && argv[1] && '-' == argv[1][0])
416 argv += 2;
417 // skip the pair
418 // N.B. do we really need this?!
419 if (!*argv++ || !*argv++)
420 break;
421 }
422 // nofail mode also clears all but IO errors (or signals)
423 if (ERR_IO != exitcode)
424 exitcode = ERR_OK;
425 }
426#endif
427 // bail out unless we expected successfully
428 if (exitcode)
429 break;
430
431 //-----------------------
432 // do send
433 //-----------------------
434 if (*argv) {
435#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
436 int nocr = 0; // inhibit terminating command with \r
437#endif
438 char *loaded = NULL; // loaded command
439 size_t len;
440 char *buf = *argv++;
441
442 // if command starts with @
443 // load "real" command from file named after @
444 if ('@' == *buf) {
445 // skip the @ and any following white-space
446 trim(++buf);
447 buf = loaded = xmalloc_xopen_read_close(buf, NULL);
448 }
449 // expand escape sequences in command
450 len = unescape(buf, &nocr);
451
452 // send command
453 alarm(timeout);
454 pfd.fd = STDOUT_FILENO;
455 pfd.events = POLLOUT;
456 while (len && !exitcode
457 && poll(&pfd, 1, -1) > 0
458 && (pfd.revents & POLLOUT)
459 ) {
460#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
461 // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
462 // "\\K" means send BREAK
463 char c = *buf;
464 if ('\\' == c) {
465 c = *++buf;
466 if ('d' == c) {
467 sleep(1);
468 len--;
469 continue;
470 }
471 if ('p' == c) {
472 usleep(10000);
473 len--;
474 continue;
475 }
476 if ('K' == c) {
477 tcsendbreak(STDOUT_FILENO, 0);
478 len--;
479 continue;
480 }
481 buf--;
482 }
483 if (safe_write(STDOUT_FILENO, buf, 1) != 1)
484 break;
485 len--;
486 buf++;
487#else
488 len -= full_write(STDOUT_FILENO, buf, len);
489#endif
490 } /* while (can write) */
491 alarm(0);
492
493 // report I/O error if there still exists at least one non-sent char
494 if (len)
495 exitcode = ERR_IO;
496
497 // free loaded command (if any)
498 if (loaded)
499 free(loaded);
500#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
501 // or terminate command with \r (if not inhibited)
502 else if (!nocr)
503 xwrite(STDOUT_FILENO, "\r", 1);
504#endif
505 // bail out unless we sent command successfully
506 if (exitcode)
507 break;
508 } /* if (*argv) */
509 }
510 } /* while (*argv) */
511
512#if ENABLE_FEATURE_CHAT_TTY_HIFI
513 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
514#endif
515
516 return exitcode;
517}
518