blob: ce994f870f50b39bd2944939e9de8f1f81e42162 [file] [log] [blame]
Denis Vlasenkoe9355082008-02-19 11:35:08 +00001/* 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 *
Denys Vlasenko0ef64bd2010-08-16 20:14:46 +02008 * Licensed under GPLv2, see file LICENSE in this source tree.
Denis Vlasenkoe9355082008-02-19 11:35:08 +00009 */
Pere Orga5bc8c002011-04-11 03:29:49 +020010
11//usage:#define chat_trivial_usage
12//usage: "EXPECT [SEND [EXPECT [SEND...]]]"
13//usage:#define chat_full_usage "\n\n"
14//usage: "Useful for interacting with a modem connected to stdin/stdout.\n"
15//usage: "A script consists of one or more \"expect-send\" pairs of strings,\n"
16//usage: "each pair is a pair of arguments. Example:\n"
17//usage: "chat '' ATZ OK ATD123456 CONNECT '' ogin: pppuser word: ppppass '~'"
18
Denis Vlasenkoe9355082008-02-19 11:35:08 +000019#include "libbb.h"
20
Denis Vlasenkoe9355082008-02-19 11:35:08 +000021// default timeout: 45 sec
Denys Vlasenkoe4dcba12010-10-28 18:57:19 +020022#define DEFAULT_CHAT_TIMEOUT 45*1000
Denis Vlasenkoe9355082008-02-19 11:35:08 +000023// max length of "abort string",
24// i.e. device reply which causes termination
25#define MAX_ABORT_LEN 50
26
27// possible exit codes
28enum {
29 ERR_OK = 0, // all's well
30 ERR_MEM, // read too much while expecting
31 ERR_IO, // signalled or I/O error
32 ERR_TIMEOUT, // timed out while expecting
33 ERR_ABORT, // first abort condition was met
34// ERR_ABORT2, // second abort condition was met
35// ...
36};
37
38// exit code
Denis Vlasenko99d71da2009-03-16 23:06:23 +000039#define exitcode bb_got_signal
Denis Vlasenkoe9355082008-02-19 11:35:08 +000040
41// trap for critical signals
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +000042static void signal_handler(UNUSED_PARAM int signo)
Denis Vlasenkoe9355082008-02-19 11:35:08 +000043{
44 // report I/O error condition
45 exitcode = ERR_IO;
46}
47
48#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
49#define unescape(s, nocr) unescape(s)
50#endif
51static size_t unescape(char *s, int *nocr)
52{
53 char *start = s;
54 char *p = s;
55
56 while (*s) {
57 char c = *s;
58 // do we need special processing?
59 // standard escapes + \s for space and \N for \0
60 // \c inhibits terminating \r for commands and is noop for expects
61 if ('\\' == c) {
62 c = *++s;
63 if (c) {
64#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
65 if ('c' == c) {
66 *nocr = 1;
67 goto next;
68 }
69#endif
70 if ('N' == c) {
71 c = '\0';
72 } else if ('s' == c) {
73 c = ' ';
74#if ENABLE_FEATURE_CHAT_NOFAIL
75 // unescape leading dash only
76 // TODO: and only for expect, not command string
77 } else if ('-' == c && (start + 1 == s)) {
78 //c = '-';
79#endif
80 } else {
81 c = bb_process_escape_sequence((const char **)&s);
82 s--;
83 }
84 }
85 // ^A becomes \001, ^B -- \002 and so on...
86 } else if ('^' == c) {
87 c = *++s-'@';
88 }
89 // put unescaped char
90 *p++ = c;
91#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
92 next:
93#endif
94 // next char
95 s++;
96 }
97 *p = '\0';
98
99 return p - start;
100}
101
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000102int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +0000103int chat_main(int argc UNUSED_PARAM, char **argv)
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000104{
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000105 int record_fd = -1;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000106 bool echo = 0;
107 // collection of device replies which cause unconditional termination
108 llist_t *aborts = NULL;
109 // inactivity period
110 int timeout = DEFAULT_CHAT_TIMEOUT;
111 // maximum length of abort string
112#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
113 size_t max_abort_len = 0;
114#else
115#define max_abort_len MAX_ABORT_LEN
116#endif
117#if ENABLE_FEATURE_CHAT_TTY_HIFI
118 struct termios tio0, tio;
119#endif
120 // directive names
121 enum {
122 DIR_HANGUP = 0,
123 DIR_ABORT,
124#if ENABLE_FEATURE_CHAT_CLR_ABORT
125 DIR_CLR_ABORT,
126#endif
127 DIR_TIMEOUT,
128 DIR_ECHO,
129 DIR_SAY,
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000130 DIR_RECORD,
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000131 };
132
133 // make x* functions fail with correct exitcode
134 xfunc_error_retval = ERR_IO;
135
136 // trap vanilla signals to prevent process from being killed suddenly
137 bb_signals(0
138 + (1 << SIGHUP)
139 + (1 << SIGINT)
140 + (1 << SIGTERM)
141 + (1 << SIGPIPE)
142 , signal_handler);
143
144#if ENABLE_FEATURE_CHAT_TTY_HIFI
145 tcgetattr(STDIN_FILENO, &tio);
146 tio0 = tio;
147 cfmakeraw(&tio);
148 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
149#endif
150
151#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
152 getopt32(argv, "vVsSE");
153 argv += optind;
154#else
155 argv++; // goto first arg
156#endif
157 // handle chat expect-send pairs
158 while (*argv) {
159 // directive given? process it
160 int key = index_in_strings(
161 "HANGUP\0" "ABORT\0"
162#if ENABLE_FEATURE_CHAT_CLR_ABORT
163 "CLR_ABORT\0"
164#endif
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000165 "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000166 , *argv
167 );
168 if (key >= 0) {
169 // cache directive value
170 char *arg = *++argv;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000171 // OFF -> 0, anything else -> 1
172 bool onoff = (0 != strcmp("OFF", arg));
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000173 // process directive
174 if (DIR_HANGUP == key) {
175 // turn SIGHUP on/off
176 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
177 } else if (DIR_ABORT == key) {
178 // append the string to abort conditions
179#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
180 size_t len = strlen(arg);
181 if (len > max_abort_len)
182 max_abort_len = len;
183#endif
184 llist_add_to_end(&aborts, arg);
185#if ENABLE_FEATURE_CHAT_CLR_ABORT
186 } else if (DIR_CLR_ABORT == key) {
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100187 llist_t *l;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000188 // remove the string from abort conditions
189 // N.B. gotta refresh maximum length too...
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100190# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000191 max_abort_len = 0;
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100192# endif
193 for (l = aborts; l; l = l->link) {
194# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000195 size_t len = strlen(l->data);
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100196# endif
197 if (strcmp(arg, l->data) == 0) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000198 llist_unlink(&aborts, l);
199 continue;
200 }
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100201# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000202 if (len > max_abort_len)
203 max_abort_len = len;
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100204# endif
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000205 }
206#endif
207 } else if (DIR_TIMEOUT == key) {
208 // set new timeout
209 // -1 means OFF
210 timeout = atoi(arg) * 1000;
211 // 0 means default
212 // >0 means value in msecs
213 if (!timeout)
214 timeout = DEFAULT_CHAT_TIMEOUT;
215 } else if (DIR_ECHO == key) {
216 // turn echo on/off
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000217 // N.B. echo means dumping device input/output to stderr
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000218 echo = onoff;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000219 } else if (DIR_RECORD == key) {
220 // turn record on/off
221 // N.B. record means dumping device input to a file
222 // close previous record_fd
223 if (record_fd > 0)
224 close(record_fd);
225 // N.B. do we have to die here on open error?
226 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000227 } else if (DIR_SAY == key) {
228 // just print argument verbatim
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000229 // TODO: should we use full_write() to avoid unistd/stdio conflict?
230 bb_error_msg("%s", arg);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000231 }
232 // next, please!
233 argv++;
234 // ordinary expect-send pair!
235 } else {
236 //-----------------------
237 // do expect
238 //-----------------------
Denis Vlasenko85c24712008-03-17 09:04:04 +0000239 int expect_len;
240 size_t buf_len = 0;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000241 size_t max_len = max_abort_len;
242
243 struct pollfd pfd;
244#if ENABLE_FEATURE_CHAT_NOFAIL
245 int nofail = 0;
246#endif
247 char *expect = *argv++;
248
249 // sanity check: shall we really expect something?
250 if (!expect)
251 goto expect_done;
252
253#if ENABLE_FEATURE_CHAT_NOFAIL
254 // if expect starts with -
255 if ('-' == *expect) {
256 // swallow -
257 expect++;
258 // and enter nofail mode
259 nofail++;
260 }
261#endif
262
263#ifdef ___TEST___BUF___ // test behaviour with a small buffer
264# undef COMMON_BUFSIZE
265# define COMMON_BUFSIZE 6
266#endif
267 // expand escape sequences in expect
268 expect_len = unescape(expect, &expect_len /*dummy*/);
269 if (expect_len > max_len)
270 max_len = expect_len;
271 // sanity check:
272 // we should expect more than nothing but not more than input buffer
273 // TODO: later we'll get rid of fixed-size buffer
274 if (!expect_len)
275 goto expect_done;
276 if (max_len >= COMMON_BUFSIZE) {
277 exitcode = ERR_MEM;
278 goto expect_done;
279 }
280
281 // get reply
282 pfd.fd = STDIN_FILENO;
283 pfd.events = POLLIN;
284 while (!exitcode
285 && poll(&pfd, 1, timeout) > 0
286 && (pfd.revents & POLLIN)
287 ) {
288#define buf bb_common_bufsiz1
289 llist_t *l;
290 ssize_t delta;
291
292 // read next char from device
293 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000294 // dump device input if RECORD fname
295 if (record_fd > 0) {
296 full_write(record_fd, buf+buf_len, 1);
297 }
298 // dump device input if ECHO ON
299 if (echo > 0) {
300// if (buf[buf_len] < ' ') {
301// full_write(STDERR_FILENO, "^", 1);
302// buf[buf_len] += '@';
303// }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000304 full_write(STDERR_FILENO, buf+buf_len, 1);
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000305 }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000306 buf_len++;
307 // move input frame if we've reached higher bound
308 if (buf_len > COMMON_BUFSIZE) {
309 memmove(buf, buf+buf_len-max_len, max_len);
310 buf_len = max_len;
311 }
312 }
313 // N.B. rule of thumb: values being looked for can
314 // be found only at the end of input buffer
315 // this allows to get rid of strstr() and memmem()
316
317 // TODO: make expect and abort strings processed uniformly
318 // abort condition is met? -> bail out
319 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
320 size_t len = strlen(l->data);
321 delta = buf_len-len;
322 if (delta >= 0 && !memcmp(buf+delta, l->data, len))
323 goto expect_done;
324 }
325 exitcode = ERR_OK;
326
327 // expected reply received? -> goto next command
Denis Vlasenko85c24712008-03-17 09:04:04 +0000328 delta = buf_len - expect_len;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000329 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
330 goto expect_done;
331#undef buf
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000332 } /* while (have data) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000333
334 // device timed out or unexpected reply received
335 exitcode = ERR_TIMEOUT;
336 expect_done:
337#if ENABLE_FEATURE_CHAT_NOFAIL
338 // on success and when in nofail mode
339 // we should skip following subsend-subexpect pairs
340 if (nofail) {
341 if (!exitcode) {
342 // find last send before non-dashed expect
343 while (*argv && argv[1] && '-' == argv[1][0])
344 argv += 2;
345 // skip the pair
346 // N.B. do we really need this?!
347 if (!*argv++ || !*argv++)
348 break;
349 }
350 // nofail mode also clears all but IO errors (or signals)
351 if (ERR_IO != exitcode)
352 exitcode = ERR_OK;
353 }
354#endif
355 // bail out unless we expected successfully
356 if (exitcode)
357 break;
358
359 //-----------------------
360 // do send
361 //-----------------------
362 if (*argv) {
363#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
364 int nocr = 0; // inhibit terminating command with \r
365#endif
366 char *loaded = NULL; // loaded command
367 size_t len;
368 char *buf = *argv++;
369
370 // if command starts with @
371 // load "real" command from file named after @
372 if ('@' == *buf) {
373 // skip the @ and any following white-space
374 trim(++buf);
Denis Vlasenkof3745ea2008-04-19 19:32:08 +0000375 buf = loaded = xmalloc_xopen_read_close(buf, NULL);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000376 }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000377 // expand escape sequences in command
378 len = unescape(buf, &nocr);
379
380 // send command
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000381 alarm(timeout);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000382 pfd.fd = STDOUT_FILENO;
383 pfd.events = POLLOUT;
384 while (len && !exitcode
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000385 && poll(&pfd, 1, -1) > 0
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000386 && (pfd.revents & POLLOUT)
387 ) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000388#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000389 // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
390 // "\\K" means send BREAK
391 char c = *buf;
392 if ('\\' == c) {
393 c = *++buf;
394 if ('d' == c) {
395 sleep(1);
396 len--;
397 continue;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000398 }
399 if ('p' == c) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000400 usleep(10000);
401 len--;
402 continue;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000403 }
404 if ('K' == c) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000405 tcsendbreak(STDOUT_FILENO, 0);
406 len--;
407 continue;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000408 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000409 buf--;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000410 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000411 if (safe_write(STDOUT_FILENO, buf, 1) != 1)
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000412 break;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000413 len--;
414 buf++;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000415#else
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000416 len -= full_write(STDOUT_FILENO, buf, len);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000417#endif
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000418 } /* while (can write) */
419 alarm(0);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000420
421 // report I/O error if there still exists at least one non-sent char
422 if (len)
423 exitcode = ERR_IO;
424
425 // free loaded command (if any)
426 if (loaded)
427 free(loaded);
428#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
429 // or terminate command with \r (if not inhibited)
430 else if (!nocr)
431 xwrite(STDOUT_FILENO, "\r", 1);
432#endif
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000433 // bail out unless we sent command successfully
434 if (exitcode)
435 break;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000436 } /* if (*argv) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000437 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000438 } /* while (*argv) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000439
440#if ENABLE_FEATURE_CHAT_TTY_HIFI
441 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
442#endif
443
444 return exitcode;
445}