blob: 6b429f2a648f00b5ccd4b5fbb2a95d311782b571 [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"
Denys Vlasenkoe6a2f4c2016-04-21 16:26:30 +020020#include "common_bufsiz.h"
Denis Vlasenkoe9355082008-02-19 11:35:08 +000021
Denis Vlasenkoe9355082008-02-19 11:35:08 +000022// default timeout: 45 sec
Denys Vlasenkoe4dcba12010-10-28 18:57:19 +020023#define DEFAULT_CHAT_TIMEOUT 45*1000
Denis Vlasenkoe9355082008-02-19 11:35:08 +000024// max length of "abort string",
25// i.e. device reply which causes termination
26#define MAX_ABORT_LEN 50
27
28// possible exit codes
29enum {
30 ERR_OK = 0, // all's well
31 ERR_MEM, // read too much while expecting
32 ERR_IO, // signalled or I/O error
33 ERR_TIMEOUT, // timed out while expecting
34 ERR_ABORT, // first abort condition was met
35// ERR_ABORT2, // second abort condition was met
36// ...
37};
38
39// exit code
Denis Vlasenko99d71da2009-03-16 23:06:23 +000040#define exitcode bb_got_signal
Denis Vlasenkoe9355082008-02-19 11:35:08 +000041
42// trap for critical signals
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +000043static void signal_handler(UNUSED_PARAM int signo)
Denis Vlasenkoe9355082008-02-19 11:35:08 +000044{
45 // report I/O error condition
46 exitcode = ERR_IO;
47}
48
49#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
50#define unescape(s, nocr) unescape(s)
51#endif
52static size_t unescape(char *s, int *nocr)
53{
54 char *start = s;
55 char *p = s;
56
57 while (*s) {
58 char c = *s;
59 // do we need special processing?
60 // standard escapes + \s for space and \N for \0
61 // \c inhibits terminating \r for commands and is noop for expects
62 if ('\\' == c) {
63 c = *++s;
64 if (c) {
65#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
66 if ('c' == c) {
67 *nocr = 1;
68 goto next;
69 }
70#endif
71 if ('N' == c) {
72 c = '\0';
73 } else if ('s' == c) {
74 c = ' ';
75#if ENABLE_FEATURE_CHAT_NOFAIL
76 // unescape leading dash only
77 // TODO: and only for expect, not command string
78 } else if ('-' == c && (start + 1 == s)) {
79 //c = '-';
80#endif
81 } else {
82 c = bb_process_escape_sequence((const char **)&s);
83 s--;
84 }
85 }
86 // ^A becomes \001, ^B -- \002 and so on...
87 } else if ('^' == c) {
88 c = *++s-'@';
89 }
90 // put unescaped char
91 *p++ = c;
92#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
93 next:
94#endif
95 // next char
96 s++;
97 }
98 *p = '\0';
99
100 return p - start;
101}
102
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000103int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +0000104int chat_main(int argc UNUSED_PARAM, char **argv)
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000105{
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000106 int record_fd = -1;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000107 bool echo = 0;
108 // collection of device replies which cause unconditional termination
109 llist_t *aborts = NULL;
110 // inactivity period
111 int timeout = DEFAULT_CHAT_TIMEOUT;
112 // maximum length of abort string
113#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
114 size_t max_abort_len = 0;
115#else
116#define max_abort_len MAX_ABORT_LEN
117#endif
118#if ENABLE_FEATURE_CHAT_TTY_HIFI
119 struct termios tio0, tio;
120#endif
121 // directive names
122 enum {
123 DIR_HANGUP = 0,
124 DIR_ABORT,
125#if ENABLE_FEATURE_CHAT_CLR_ABORT
126 DIR_CLR_ABORT,
127#endif
128 DIR_TIMEOUT,
129 DIR_ECHO,
130 DIR_SAY,
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000131 DIR_RECORD,
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000132 };
133
134 // make x* functions fail with correct exitcode
135 xfunc_error_retval = ERR_IO;
136
137 // trap vanilla signals to prevent process from being killed suddenly
138 bb_signals(0
139 + (1 << SIGHUP)
140 + (1 << SIGINT)
141 + (1 << SIGTERM)
142 + (1 << SIGPIPE)
143 , signal_handler);
144
145#if ENABLE_FEATURE_CHAT_TTY_HIFI
146 tcgetattr(STDIN_FILENO, &tio);
147 tio0 = tio;
148 cfmakeraw(&tio);
149 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
150#endif
151
152#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
153 getopt32(argv, "vVsSE");
154 argv += optind;
155#else
156 argv++; // goto first arg
157#endif
158 // handle chat expect-send pairs
159 while (*argv) {
160 // directive given? process it
161 int key = index_in_strings(
162 "HANGUP\0" "ABORT\0"
163#if ENABLE_FEATURE_CHAT_CLR_ABORT
164 "CLR_ABORT\0"
165#endif
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000166 "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000167 , *argv
168 );
169 if (key >= 0) {
170 // cache directive value
171 char *arg = *++argv;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000172 // OFF -> 0, anything else -> 1
173 bool onoff = (0 != strcmp("OFF", arg));
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000174 // process directive
175 if (DIR_HANGUP == key) {
176 // turn SIGHUP on/off
177 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
178 } else if (DIR_ABORT == key) {
179 // append the string to abort conditions
180#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
181 size_t len = strlen(arg);
182 if (len > max_abort_len)
183 max_abort_len = len;
184#endif
185 llist_add_to_end(&aborts, arg);
186#if ENABLE_FEATURE_CHAT_CLR_ABORT
187 } else if (DIR_CLR_ABORT == key) {
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100188 llist_t *l;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000189 // remove the string from abort conditions
190 // N.B. gotta refresh maximum length too...
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100191# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000192 max_abort_len = 0;
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100193# endif
194 for (l = aborts; l; l = l->link) {
195# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000196 size_t len = strlen(l->data);
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100197# endif
198 if (strcmp(arg, l->data) == 0) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000199 llist_unlink(&aborts, l);
200 continue;
201 }
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100202# if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000203 if (len > max_abort_len)
204 max_abort_len = len;
Denys Vlasenkoa3aea322010-12-18 01:42:50 +0100205# endif
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000206 }
207#endif
208 } else if (DIR_TIMEOUT == key) {
209 // set new timeout
210 // -1 means OFF
211 timeout = atoi(arg) * 1000;
212 // 0 means default
213 // >0 means value in msecs
214 if (!timeout)
215 timeout = DEFAULT_CHAT_TIMEOUT;
216 } else if (DIR_ECHO == key) {
217 // turn echo on/off
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000218 // N.B. echo means dumping device input/output to stderr
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000219 echo = onoff;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000220 } else if (DIR_RECORD == key) {
221 // turn record on/off
222 // N.B. record means dumping device input to a file
223 // close previous record_fd
224 if (record_fd > 0)
225 close(record_fd);
226 // N.B. do we have to die here on open error?
227 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000228 } else if (DIR_SAY == key) {
229 // just print argument verbatim
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000230 // TODO: should we use full_write() to avoid unistd/stdio conflict?
231 bb_error_msg("%s", arg);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000232 }
233 // next, please!
234 argv++;
235 // ordinary expect-send pair!
236 } else {
237 //-----------------------
238 // do expect
239 //-----------------------
Denis Vlasenko85c24712008-03-17 09:04:04 +0000240 int expect_len;
241 size_t buf_len = 0;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000242 size_t max_len = max_abort_len;
243
244 struct pollfd pfd;
245#if ENABLE_FEATURE_CHAT_NOFAIL
246 int nofail = 0;
247#endif
248 char *expect = *argv++;
249
250 // sanity check: shall we really expect something?
251 if (!expect)
252 goto expect_done;
253
254#if ENABLE_FEATURE_CHAT_NOFAIL
255 // if expect starts with -
256 if ('-' == *expect) {
257 // swallow -
258 expect++;
259 // and enter nofail mode
260 nofail++;
261 }
262#endif
263
264#ifdef ___TEST___BUF___ // test behaviour with a small buffer
265# undef COMMON_BUFSIZE
266# define COMMON_BUFSIZE 6
267#endif
268 // expand escape sequences in expect
269 expect_len = unescape(expect, &expect_len /*dummy*/);
270 if (expect_len > max_len)
271 max_len = expect_len;
272 // sanity check:
273 // we should expect more than nothing but not more than input buffer
274 // TODO: later we'll get rid of fixed-size buffer
275 if (!expect_len)
276 goto expect_done;
277 if (max_len >= COMMON_BUFSIZE) {
278 exitcode = ERR_MEM;
279 goto expect_done;
280 }
281
282 // get reply
283 pfd.fd = STDIN_FILENO;
284 pfd.events = POLLIN;
285 while (!exitcode
286 && poll(&pfd, 1, timeout) > 0
287 && (pfd.revents & POLLIN)
288 ) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000289 llist_t *l;
290 ssize_t delta;
Denys Vlasenko9de2e5a2016-04-21 18:38:51 +0200291#define buf bb_common_bufsiz1
292 setup_common_bufsiz();
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000293
294 // read next char from device
295 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000296 // dump device input if RECORD fname
297 if (record_fd > 0) {
298 full_write(record_fd, buf+buf_len, 1);
299 }
300 // dump device input if ECHO ON
Denys Vlasenko9b2a9f02013-11-29 16:43:33 +0100301 if (echo) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000302// if (buf[buf_len] < ' ') {
303// full_write(STDERR_FILENO, "^", 1);
304// buf[buf_len] += '@';
305// }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000306 full_write(STDERR_FILENO, buf+buf_len, 1);
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000307 }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000308 buf_len++;
309 // move input frame if we've reached higher bound
310 if (buf_len > COMMON_BUFSIZE) {
311 memmove(buf, buf+buf_len-max_len, max_len);
312 buf_len = max_len;
313 }
314 }
315 // N.B. rule of thumb: values being looked for can
316 // be found only at the end of input buffer
317 // this allows to get rid of strstr() and memmem()
318
319 // TODO: make expect and abort strings processed uniformly
320 // abort condition is met? -> bail out
321 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
322 size_t len = strlen(l->data);
323 delta = buf_len-len;
324 if (delta >= 0 && !memcmp(buf+delta, l->data, len))
325 goto expect_done;
326 }
327 exitcode = ERR_OK;
328
329 // expected reply received? -> goto next command
Denis Vlasenko85c24712008-03-17 09:04:04 +0000330 delta = buf_len - expect_len;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000331 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
332 goto expect_done;
333#undef buf
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000334 } /* while (have data) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000335
336 // device timed out or unexpected reply received
337 exitcode = ERR_TIMEOUT;
338 expect_done:
339#if ENABLE_FEATURE_CHAT_NOFAIL
340 // on success and when in nofail mode
341 // we should skip following subsend-subexpect pairs
342 if (nofail) {
343 if (!exitcode) {
344 // find last send before non-dashed expect
345 while (*argv && argv[1] && '-' == argv[1][0])
346 argv += 2;
347 // skip the pair
348 // N.B. do we really need this?!
349 if (!*argv++ || !*argv++)
350 break;
351 }
352 // nofail mode also clears all but IO errors (or signals)
353 if (ERR_IO != exitcode)
354 exitcode = ERR_OK;
355 }
356#endif
357 // bail out unless we expected successfully
358 if (exitcode)
359 break;
360
361 //-----------------------
362 // do send
363 //-----------------------
364 if (*argv) {
365#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
366 int nocr = 0; // inhibit terminating command with \r
367#endif
368 char *loaded = NULL; // loaded command
369 size_t len;
370 char *buf = *argv++;
371
372 // if command starts with @
373 // load "real" command from file named after @
374 if ('@' == *buf) {
375 // skip the @ and any following white-space
376 trim(++buf);
Denis Vlasenkof3745ea2008-04-19 19:32:08 +0000377 buf = loaded = xmalloc_xopen_read_close(buf, NULL);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000378 }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000379 // expand escape sequences in command
380 len = unescape(buf, &nocr);
381
382 // send command
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000383 alarm(timeout);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000384 pfd.fd = STDOUT_FILENO;
385 pfd.events = POLLOUT;
386 while (len && !exitcode
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000387 && poll(&pfd, 1, -1) > 0
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000388 && (pfd.revents & POLLOUT)
389 ) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000390#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000391 // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
392 // "\\K" means send BREAK
393 char c = *buf;
394 if ('\\' == c) {
395 c = *++buf;
396 if ('d' == c) {
397 sleep(1);
398 len--;
399 continue;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000400 }
401 if ('p' == c) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000402 usleep(10000);
403 len--;
404 continue;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000405 }
406 if ('K' == c) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000407 tcsendbreak(STDOUT_FILENO, 0);
408 len--;
409 continue;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000410 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000411 buf--;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000412 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000413 if (safe_write(STDOUT_FILENO, buf, 1) != 1)
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000414 break;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000415 len--;
416 buf++;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000417#else
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000418 len -= full_write(STDOUT_FILENO, buf, len);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000419#endif
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000420 } /* while (can write) */
421 alarm(0);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000422
423 // report I/O error if there still exists at least one non-sent char
424 if (len)
425 exitcode = ERR_IO;
426
427 // free loaded command (if any)
428 if (loaded)
429 free(loaded);
430#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
431 // or terminate command with \r (if not inhibited)
432 else if (!nocr)
433 xwrite(STDOUT_FILENO, "\r", 1);
434#endif
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000435 // bail out unless we sent command successfully
436 if (exitcode)
437 break;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000438 } /* if (*argv) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000439 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000440 } /* while (*argv) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000441
442#if ENABLE_FEATURE_CHAT_TTY_HIFI
443 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
444#endif
445
446 return exitcode;
447}