blob: 3ffd7b228fb0d980d4d03ea56ef40509bae37d5f [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 *
8 * Licensed under GPLv2, see file LICENSE in this tarball for details.
9 */
10#include "libbb.h"
11
Denis Vlasenkoe9355082008-02-19 11:35:08 +000012// default timeout: 45 sec
13#define DEFAULT_CHAT_TIMEOUT 45*1000
14// max length of "abort string",
15// i.e. device reply which causes termination
16#define MAX_ABORT_LEN 50
17
18// possible exit codes
19enum {
20 ERR_OK = 0, // all's well
21 ERR_MEM, // read too much while expecting
22 ERR_IO, // signalled or I/O error
23 ERR_TIMEOUT, // timed out while expecting
24 ERR_ABORT, // first abort condition was met
25// ERR_ABORT2, // second abort condition was met
26// ...
27};
28
29// exit code
Denis Vlasenko99d71da2009-03-16 23:06:23 +000030#define exitcode bb_got_signal
Denis Vlasenkoe9355082008-02-19 11:35:08 +000031
32// trap for critical signals
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +000033static void signal_handler(UNUSED_PARAM int signo)
Denis Vlasenkoe9355082008-02-19 11:35:08 +000034{
35 // report I/O error condition
36 exitcode = ERR_IO;
37}
38
39#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
40#define unescape(s, nocr) unescape(s)
41#endif
42static size_t unescape(char *s, int *nocr)
43{
44 char *start = s;
45 char *p = s;
46
47 while (*s) {
48 char c = *s;
49 // do we need special processing?
50 // standard escapes + \s for space and \N for \0
51 // \c inhibits terminating \r for commands and is noop for expects
52 if ('\\' == c) {
53 c = *++s;
54 if (c) {
55#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
56 if ('c' == c) {
57 *nocr = 1;
58 goto next;
59 }
60#endif
61 if ('N' == c) {
62 c = '\0';
63 } else if ('s' == c) {
64 c = ' ';
65#if ENABLE_FEATURE_CHAT_NOFAIL
66 // unescape leading dash only
67 // TODO: and only for expect, not command string
68 } else if ('-' == c && (start + 1 == s)) {
69 //c = '-';
70#endif
71 } else {
72 c = bb_process_escape_sequence((const char **)&s);
73 s--;
74 }
75 }
76 // ^A becomes \001, ^B -- \002 and so on...
77 } else if ('^' == c) {
78 c = *++s-'@';
79 }
80 // put unescaped char
81 *p++ = c;
82#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
83 next:
84#endif
85 // next char
86 s++;
87 }
88 *p = '\0';
89
90 return p - start;
91}
92
Denis Vlasenkoe9355082008-02-19 11:35:08 +000093int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +000094int chat_main(int argc UNUSED_PARAM, char **argv)
Denis Vlasenkoe9355082008-02-19 11:35:08 +000095{
Denis Vlasenko99d71da2009-03-16 23:06:23 +000096 int record_fd = -1;
Denis Vlasenkoe9355082008-02-19 11:35:08 +000097 bool echo = 0;
98 // collection of device replies which cause unconditional termination
99 llist_t *aborts = NULL;
100 // inactivity period
101 int timeout = DEFAULT_CHAT_TIMEOUT;
102 // maximum length of abort string
103#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
104 size_t max_abort_len = 0;
105#else
106#define max_abort_len MAX_ABORT_LEN
107#endif
108#if ENABLE_FEATURE_CHAT_TTY_HIFI
109 struct termios tio0, tio;
110#endif
111 // directive names
112 enum {
113 DIR_HANGUP = 0,
114 DIR_ABORT,
115#if ENABLE_FEATURE_CHAT_CLR_ABORT
116 DIR_CLR_ABORT,
117#endif
118 DIR_TIMEOUT,
119 DIR_ECHO,
120 DIR_SAY,
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000121 DIR_RECORD,
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000122 };
123
124 // make x* functions fail with correct exitcode
125 xfunc_error_retval = ERR_IO;
126
127 // trap vanilla signals to prevent process from being killed suddenly
128 bb_signals(0
129 + (1 << SIGHUP)
130 + (1 << SIGINT)
131 + (1 << SIGTERM)
132 + (1 << SIGPIPE)
133 , signal_handler);
134
135#if ENABLE_FEATURE_CHAT_TTY_HIFI
136 tcgetattr(STDIN_FILENO, &tio);
137 tio0 = tio;
138 cfmakeraw(&tio);
139 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
140#endif
141
142#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
143 getopt32(argv, "vVsSE");
144 argv += optind;
145#else
146 argv++; // goto first arg
147#endif
148 // handle chat expect-send pairs
149 while (*argv) {
150 // directive given? process it
151 int key = index_in_strings(
152 "HANGUP\0" "ABORT\0"
153#if ENABLE_FEATURE_CHAT_CLR_ABORT
154 "CLR_ABORT\0"
155#endif
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000156 "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000157 , *argv
158 );
159 if (key >= 0) {
160 // cache directive value
161 char *arg = *++argv;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000162 // OFF -> 0, anything else -> 1
163 bool onoff = (0 != strcmp("OFF", arg));
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000164 // process directive
165 if (DIR_HANGUP == key) {
166 // turn SIGHUP on/off
167 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
168 } else if (DIR_ABORT == key) {
169 // append the string to abort conditions
170#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
171 size_t len = strlen(arg);
172 if (len > max_abort_len)
173 max_abort_len = len;
174#endif
175 llist_add_to_end(&aborts, arg);
176#if ENABLE_FEATURE_CHAT_CLR_ABORT
177 } else if (DIR_CLR_ABORT == key) {
178 // remove the string from abort conditions
179 // N.B. gotta refresh maximum length too...
180#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
181 max_abort_len = 0;
182#endif
183 for (llist_t *l = aborts; l; l = l->link) {
184#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
185 size_t len = strlen(l->data);
186#endif
187 if (!strcmp(arg, l->data)) {
188 llist_unlink(&aborts, l);
189 continue;
190 }
191#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
192 if (len > max_abort_len)
193 max_abort_len = len;
194#endif
195 }
196#endif
197 } else if (DIR_TIMEOUT == key) {
198 // set new timeout
199 // -1 means OFF
200 timeout = atoi(arg) * 1000;
201 // 0 means default
202 // >0 means value in msecs
203 if (!timeout)
204 timeout = DEFAULT_CHAT_TIMEOUT;
205 } else if (DIR_ECHO == key) {
206 // turn echo on/off
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000207 // N.B. echo means dumping device input/output to stderr
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000208 echo = onoff;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000209 } else if (DIR_RECORD == key) {
210 // turn record on/off
211 // N.B. record means dumping device input to a file
212 // close previous record_fd
213 if (record_fd > 0)
214 close(record_fd);
215 // N.B. do we have to die here on open error?
216 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000217 } else if (DIR_SAY == key) {
218 // just print argument verbatim
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000219 // TODO: should we use full_write() to avoid unistd/stdio conflict?
220 bb_error_msg("%s", arg);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000221 }
222 // next, please!
223 argv++;
224 // ordinary expect-send pair!
225 } else {
226 //-----------------------
227 // do expect
228 //-----------------------
Denis Vlasenko85c24712008-03-17 09:04:04 +0000229 int expect_len;
230 size_t buf_len = 0;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000231 size_t max_len = max_abort_len;
232
233 struct pollfd pfd;
234#if ENABLE_FEATURE_CHAT_NOFAIL
235 int nofail = 0;
236#endif
237 char *expect = *argv++;
238
239 // sanity check: shall we really expect something?
240 if (!expect)
241 goto expect_done;
242
243#if ENABLE_FEATURE_CHAT_NOFAIL
244 // if expect starts with -
245 if ('-' == *expect) {
246 // swallow -
247 expect++;
248 // and enter nofail mode
249 nofail++;
250 }
251#endif
252
253#ifdef ___TEST___BUF___ // test behaviour with a small buffer
254# undef COMMON_BUFSIZE
255# define COMMON_BUFSIZE 6
256#endif
257 // expand escape sequences in expect
258 expect_len = unescape(expect, &expect_len /*dummy*/);
259 if (expect_len > max_len)
260 max_len = expect_len;
261 // sanity check:
262 // we should expect more than nothing but not more than input buffer
263 // TODO: later we'll get rid of fixed-size buffer
264 if (!expect_len)
265 goto expect_done;
266 if (max_len >= COMMON_BUFSIZE) {
267 exitcode = ERR_MEM;
268 goto expect_done;
269 }
270
271 // get reply
272 pfd.fd = STDIN_FILENO;
273 pfd.events = POLLIN;
274 while (!exitcode
275 && poll(&pfd, 1, timeout) > 0
276 && (pfd.revents & POLLIN)
277 ) {
278#define buf bb_common_bufsiz1
279 llist_t *l;
280 ssize_t delta;
281
282 // read next char from device
283 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000284 // dump device input if RECORD fname
285 if (record_fd > 0) {
286 full_write(record_fd, buf+buf_len, 1);
287 }
288 // dump device input if ECHO ON
289 if (echo > 0) {
290// if (buf[buf_len] < ' ') {
291// full_write(STDERR_FILENO, "^", 1);
292// buf[buf_len] += '@';
293// }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000294 full_write(STDERR_FILENO, buf+buf_len, 1);
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000295 }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000296 buf_len++;
297 // move input frame if we've reached higher bound
298 if (buf_len > COMMON_BUFSIZE) {
299 memmove(buf, buf+buf_len-max_len, max_len);
300 buf_len = max_len;
301 }
302 }
303 // N.B. rule of thumb: values being looked for can
304 // be found only at the end of input buffer
305 // this allows to get rid of strstr() and memmem()
306
307 // TODO: make expect and abort strings processed uniformly
308 // abort condition is met? -> bail out
309 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
310 size_t len = strlen(l->data);
311 delta = buf_len-len;
312 if (delta >= 0 && !memcmp(buf+delta, l->data, len))
313 goto expect_done;
314 }
315 exitcode = ERR_OK;
316
317 // expected reply received? -> goto next command
Denis Vlasenko85c24712008-03-17 09:04:04 +0000318 delta = buf_len - expect_len;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000319 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
320 goto expect_done;
321#undef buf
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000322 } /* while (have data) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000323
324 // device timed out or unexpected reply received
325 exitcode = ERR_TIMEOUT;
326 expect_done:
327#if ENABLE_FEATURE_CHAT_NOFAIL
328 // on success and when in nofail mode
329 // we should skip following subsend-subexpect pairs
330 if (nofail) {
331 if (!exitcode) {
332 // find last send before non-dashed expect
333 while (*argv && argv[1] && '-' == argv[1][0])
334 argv += 2;
335 // skip the pair
336 // N.B. do we really need this?!
337 if (!*argv++ || !*argv++)
338 break;
339 }
340 // nofail mode also clears all but IO errors (or signals)
341 if (ERR_IO != exitcode)
342 exitcode = ERR_OK;
343 }
344#endif
345 // bail out unless we expected successfully
346 if (exitcode)
347 break;
348
349 //-----------------------
350 // do send
351 //-----------------------
352 if (*argv) {
353#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
354 int nocr = 0; // inhibit terminating command with \r
355#endif
356 char *loaded = NULL; // loaded command
357 size_t len;
358 char *buf = *argv++;
359
360 // if command starts with @
361 // load "real" command from file named after @
362 if ('@' == *buf) {
363 // skip the @ and any following white-space
364 trim(++buf);
Denis Vlasenkof3745ea2008-04-19 19:32:08 +0000365 buf = loaded = xmalloc_xopen_read_close(buf, NULL);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000366 }
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000367 // expand escape sequences in command
368 len = unescape(buf, &nocr);
369
370 // send command
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000371 alarm(timeout);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000372 pfd.fd = STDOUT_FILENO;
373 pfd.events = POLLOUT;
374 while (len && !exitcode
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000375 && poll(&pfd, 1, -1) > 0
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000376 && (pfd.revents & POLLOUT)
377 ) {
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000378#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000379 // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
380 // "\\K" means send BREAK
381 char c = *buf;
382 if ('\\' == c) {
383 c = *++buf;
384 if ('d' == c) {
385 sleep(1);
386 len--;
387 continue;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000388 }
389 if ('p' == c) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000390 usleep(10000);
391 len--;
392 continue;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000393 }
394 if ('K' == c) {
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000395 tcsendbreak(STDOUT_FILENO, 0);
396 len--;
397 continue;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000398 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000399 buf--;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000400 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000401 if (safe_write(STDOUT_FILENO, buf, 1) != 1)
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000402 break;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000403 len--;
404 buf++;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000405#else
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000406 len -= full_write(STDOUT_FILENO, buf, len);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000407#endif
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000408 } /* while (can write) */
409 alarm(0);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000410
411 // report I/O error if there still exists at least one non-sent char
412 if (len)
413 exitcode = ERR_IO;
414
415 // free loaded command (if any)
416 if (loaded)
417 free(loaded);
418#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
419 // or terminate command with \r (if not inhibited)
420 else if (!nocr)
421 xwrite(STDOUT_FILENO, "\r", 1);
422#endif
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000423 // bail out unless we sent command successfully
424 if (exitcode)
425 break;
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000426 } /* if (*argv) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000427 }
Denis Vlasenko99d71da2009-03-16 23:06:23 +0000428 } /* while (*argv) */
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000429
430#if ENABLE_FEATURE_CHAT_TTY_HIFI
431 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
432#endif
433
434 return exitcode;
435}