blob: d550c7c90af010abe92bf226c890b4783c8c5111 [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
12/*
13#define ENABLE_FEATURE_CHAT_NOFAIL 1 // +126 bytes
14#define ENABLE_FEATURE_CHAT_TTY_HIFI 0 // + 70 bytes
15#define ENABLE_FEATURE_CHAT_IMPLICIT_CR 1 // + 44 bytes
16#define ENABLE_FEATURE_CHAT_SEND_ESCAPES 0 // +103 bytes
17#define ENABLE_FEATURE_CHAT_VAR_ABORT_LEN 0 // + 70 bytes
18#define ENABLE_FEATURE_CHAT_CLR_ABORT 0 // +113 bytes
19#define ENABLE_FEATURE_CHAT_SWALLOW_OPTS 0 // + 23 bytes
20*/
21
22// default timeout: 45 sec
23#define DEFAULT_CHAT_TIMEOUT 45*1000
24// 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
40// N.B> 10 bytes for volatile. Why all these signals?!
41static /*volatile*/ smallint exitcode;
42
43// trap for critical signals
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +000044static void signal_handler(UNUSED_PARAM int signo)
Denis Vlasenkoe9355082008-02-19 11:35:08 +000045{
46 // report I/O error condition
47 exitcode = ERR_IO;
48}
49
50#if !ENABLE_FEATURE_CHAT_IMPLICIT_CR
51#define unescape(s, nocr) unescape(s)
52#endif
53static size_t unescape(char *s, int *nocr)
54{
55 char *start = s;
56 char *p = s;
57
58 while (*s) {
59 char c = *s;
60 // do we need special processing?
61 // standard escapes + \s for space and \N for \0
62 // \c inhibits terminating \r for commands and is noop for expects
63 if ('\\' == c) {
64 c = *++s;
65 if (c) {
66#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
67 if ('c' == c) {
68 *nocr = 1;
69 goto next;
70 }
71#endif
72 if ('N' == c) {
73 c = '\0';
74 } else if ('s' == c) {
75 c = ' ';
76#if ENABLE_FEATURE_CHAT_NOFAIL
77 // unescape leading dash only
78 // TODO: and only for expect, not command string
79 } else if ('-' == c && (start + 1 == s)) {
80 //c = '-';
81#endif
82 } else {
83 c = bb_process_escape_sequence((const char **)&s);
84 s--;
85 }
86 }
87 // ^A becomes \001, ^B -- \002 and so on...
88 } else if ('^' == c) {
89 c = *++s-'@';
90 }
91 // put unescaped char
92 *p++ = c;
93#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
94 next:
95#endif
96 // next char
97 s++;
98 }
99 *p = '\0';
100
101 return p - start;
102}
103
104
105int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
Denis Vlasenkoa60f84e2008-07-05 09:18:54 +0000106int chat_main(int argc UNUSED_PARAM, char **argv)
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000107{
108// should we dump device output? to what fd? by default no.
109// this can be controlled later via ECHO {ON|OFF} chat directive
110// int echo_fd;
111 bool echo = 0;
112 // collection of device replies which cause unconditional termination
113 llist_t *aborts = NULL;
114 // inactivity period
115 int timeout = DEFAULT_CHAT_TIMEOUT;
116 // maximum length of abort string
117#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
118 size_t max_abort_len = 0;
119#else
120#define max_abort_len MAX_ABORT_LEN
121#endif
122#if ENABLE_FEATURE_CHAT_TTY_HIFI
123 struct termios tio0, tio;
124#endif
125 // directive names
126 enum {
127 DIR_HANGUP = 0,
128 DIR_ABORT,
129#if ENABLE_FEATURE_CHAT_CLR_ABORT
130 DIR_CLR_ABORT,
131#endif
132 DIR_TIMEOUT,
133 DIR_ECHO,
134 DIR_SAY,
135 };
136
137 // make x* functions fail with correct exitcode
138 xfunc_error_retval = ERR_IO;
139
140 // trap vanilla signals to prevent process from being killed suddenly
141 bb_signals(0
142 + (1 << SIGHUP)
143 + (1 << SIGINT)
144 + (1 << SIGTERM)
145 + (1 << SIGPIPE)
146 , signal_handler);
147
148#if ENABLE_FEATURE_CHAT_TTY_HIFI
149 tcgetattr(STDIN_FILENO, &tio);
150 tio0 = tio;
151 cfmakeraw(&tio);
152 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
153#endif
154
155#if ENABLE_FEATURE_CHAT_SWALLOW_OPTS
156 getopt32(argv, "vVsSE");
157 argv += optind;
158#else
159 argv++; // goto first arg
160#endif
161 // handle chat expect-send pairs
162 while (*argv) {
163 // directive given? process it
164 int key = index_in_strings(
165 "HANGUP\0" "ABORT\0"
166#if ENABLE_FEATURE_CHAT_CLR_ABORT
167 "CLR_ABORT\0"
168#endif
169 "TIMEOUT\0" "ECHO\0" "SAY\0"
170 , *argv
171 );
172 if (key >= 0) {
173 // cache directive value
174 char *arg = *++argv;
175 // ON -> 1, anything else -> 0
176 bool onoff = !strcmp("ON", arg);
177 // process directive
178 if (DIR_HANGUP == key) {
179 // turn SIGHUP on/off
180 signal(SIGHUP, onoff ? signal_handler : SIG_IGN);
181 } else if (DIR_ABORT == key) {
182 // append the string to abort conditions
183#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
184 size_t len = strlen(arg);
185 if (len > max_abort_len)
186 max_abort_len = len;
187#endif
188 llist_add_to_end(&aborts, arg);
189#if ENABLE_FEATURE_CHAT_CLR_ABORT
190 } else if (DIR_CLR_ABORT == key) {
191 // remove the string from abort conditions
192 // N.B. gotta refresh maximum length too...
193#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
194 max_abort_len = 0;
195#endif
196 for (llist_t *l = aborts; l; l = l->link) {
197#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
198 size_t len = strlen(l->data);
199#endif
200 if (!strcmp(arg, l->data)) {
201 llist_unlink(&aborts, l);
202 continue;
203 }
204#if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
205 if (len > max_abort_len)
206 max_abort_len = len;
207#endif
208 }
209#endif
210 } else if (DIR_TIMEOUT == key) {
211 // set new timeout
212 // -1 means OFF
213 timeout = atoi(arg) * 1000;
214 // 0 means default
215 // >0 means value in msecs
216 if (!timeout)
217 timeout = DEFAULT_CHAT_TIMEOUT;
218 } else if (DIR_ECHO == key) {
219 // turn echo on/off
220 // N.B. echo means dumping output
221 // from stdin (device) to stderr
222 echo = onoff;
223//TODO? echo_fd = onoff * STDERR_FILENO;
224//TODO? echo_fd = xopen(arg, O_WRONLY|O_CREAT|O_TRUNC);
225 } else if (DIR_SAY == key) {
226 // just print argument verbatim
227 fprintf(stderr, arg);
228 }
229 // next, please!
230 argv++;
231 // ordinary expect-send pair!
232 } else {
233 //-----------------------
234 // do expect
235 //-----------------------
Denis Vlasenko85c24712008-03-17 09:04:04 +0000236 int expect_len;
237 size_t buf_len = 0;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000238 size_t max_len = max_abort_len;
239
240 struct pollfd pfd;
241#if ENABLE_FEATURE_CHAT_NOFAIL
242 int nofail = 0;
243#endif
244 char *expect = *argv++;
245
246 // sanity check: shall we really expect something?
247 if (!expect)
248 goto expect_done;
249
250#if ENABLE_FEATURE_CHAT_NOFAIL
251 // if expect starts with -
252 if ('-' == *expect) {
253 // swallow -
254 expect++;
255 // and enter nofail mode
256 nofail++;
257 }
258#endif
259
260#ifdef ___TEST___BUF___ // test behaviour with a small buffer
261# undef COMMON_BUFSIZE
262# define COMMON_BUFSIZE 6
263#endif
264 // expand escape sequences in expect
265 expect_len = unescape(expect, &expect_len /*dummy*/);
266 if (expect_len > max_len)
267 max_len = expect_len;
268 // sanity check:
269 // we should expect more than nothing but not more than input buffer
270 // TODO: later we'll get rid of fixed-size buffer
271 if (!expect_len)
272 goto expect_done;
273 if (max_len >= COMMON_BUFSIZE) {
274 exitcode = ERR_MEM;
275 goto expect_done;
276 }
277
278 // get reply
279 pfd.fd = STDIN_FILENO;
280 pfd.events = POLLIN;
281 while (!exitcode
282 && poll(&pfd, 1, timeout) > 0
283 && (pfd.revents & POLLIN)
284 ) {
285#define buf bb_common_bufsiz1
286 llist_t *l;
287 ssize_t delta;
288
289 // read next char from device
290 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
291 // dump device output if ECHO ON or RECORD fname
292//TODO? if (echo_fd > 0) {
293//TODO? full_write(echo_fd, buf+buf_len, 1);
294//TODO? }
295 if (echo > 0)
296 full_write(STDERR_FILENO, buf+buf_len, 1);
297 buf_len++;
298 // move input frame if we've reached higher bound
299 if (buf_len > COMMON_BUFSIZE) {
300 memmove(buf, buf+buf_len-max_len, max_len);
301 buf_len = max_len;
302 }
303 }
304 // N.B. rule of thumb: values being looked for can
305 // be found only at the end of input buffer
306 // this allows to get rid of strstr() and memmem()
307
308 // TODO: make expect and abort strings processed uniformly
309 // abort condition is met? -> bail out
310 for (l = aborts, exitcode = ERR_ABORT; l; l = l->link, ++exitcode) {
311 size_t len = strlen(l->data);
312 delta = buf_len-len;
313 if (delta >= 0 && !memcmp(buf+delta, l->data, len))
314 goto expect_done;
315 }
316 exitcode = ERR_OK;
317
318 // expected reply received? -> goto next command
Denis Vlasenko85c24712008-03-17 09:04:04 +0000319 delta = buf_len - expect_len;
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000320 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
321 goto expect_done;
322#undef buf
323 }
324
325 // device timed out or unexpected reply received
326 exitcode = ERR_TIMEOUT;
327 expect_done:
328#if ENABLE_FEATURE_CHAT_NOFAIL
329 // on success and when in nofail mode
330 // we should skip following subsend-subexpect pairs
331 if (nofail) {
332 if (!exitcode) {
333 // find last send before non-dashed expect
334 while (*argv && argv[1] && '-' == argv[1][0])
335 argv += 2;
336 // skip the pair
337 // N.B. do we really need this?!
338 if (!*argv++ || !*argv++)
339 break;
340 }
341 // nofail mode also clears all but IO errors (or signals)
342 if (ERR_IO != exitcode)
343 exitcode = ERR_OK;
344 }
345#endif
346 // bail out unless we expected successfully
347 if (exitcode)
348 break;
349
350 //-----------------------
351 // do send
352 //-----------------------
353 if (*argv) {
354#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
355 int nocr = 0; // inhibit terminating command with \r
356#endif
357 char *loaded = NULL; // loaded command
358 size_t len;
359 char *buf = *argv++;
360
361 // if command starts with @
362 // load "real" command from file named after @
363 if ('@' == *buf) {
364 // skip the @ and any following white-space
365 trim(++buf);
Denis Vlasenkof3745ea2008-04-19 19:32:08 +0000366 buf = loaded = xmalloc_xopen_read_close(buf, NULL);
Denis Vlasenkoe9355082008-02-19 11:35:08 +0000367 }
368
369 // expand escape sequences in command
370 len = unescape(buf, &nocr);
371
372 // send command
373#if ENABLE_FEATURE_CHAT_SEND_ESCAPES
374 pfd.fd = STDOUT_FILENO;
375 pfd.events = POLLOUT;
376 while (len && !exitcode
377 && poll(&pfd, 1, timeout) > 0
378 && (pfd.revents & POLLOUT)
379 ) {
380 // ugly! ugly! ugly!
381 // gotta send char by char to achieve this!
382 // Brrr...
383 // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
384 // "\\K" means send BREAK
385 char c = *buf;
386 if ('\\' == c) {
387 c = *++buf;
388 if ('d' == c) {
389 sleep(1);
390 len--;
391 continue;
392 } else if ('p' == c) {
393 usleep(10000);
394 len--;
395 continue;
396 } else if ('K' == c) {
397 tcsendbreak(STDOUT_FILENO, 0);
398 len--;
399 continue;
400 } else {
401 buf--;
402 }
403 }
404 if (safe_write(STDOUT_FILENO, buf, 1) > 0) {
405 len--;
406 buf++;
407 } else
408 break;
409 }
410#else
411// if (len) {
412 alarm(timeout);
413 len -= full_write(STDOUT_FILENO, buf, len);
414 alarm(0);
415// }
416#endif
417
418 // report I/O error if there still exists at least one non-sent char
419 if (len)
420 exitcode = ERR_IO;
421
422 // free loaded command (if any)
423 if (loaded)
424 free(loaded);
425#if ENABLE_FEATURE_CHAT_IMPLICIT_CR
426 // or terminate command with \r (if not inhibited)
427 else if (!nocr)
428 xwrite(STDOUT_FILENO, "\r", 1);
429#endif
430
431 // bail out unless we sent command successfully
432 if (exitcode)
433 break;
434
435 }
436 }
437 }
438
439#if ENABLE_FEATURE_CHAT_TTY_HIFI
440 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
441#endif
442
443 return exitcode;
444}