runlevel/wall: depend on utmp feature
[platform/upstream/busybox.git] / miscutils / chat.c
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 #include "libbb.h"
11
12 // 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
19 enum {
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
30 #define exitcode bb_got_signal
31
32 // trap for critical signals
33 static void signal_handler(UNUSED_PARAM int signo)
34 {
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
42 static 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
93 int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
94 int chat_main(int argc UNUSED_PARAM, char **argv)
95 {
96         int record_fd = -1;
97         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,
121                 DIR_RECORD,
122         };
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
156                         "TIMEOUT\0" "ECHO\0" "SAY\0" "RECORD\0"
157                         , *argv
158                 );
159                 if (key >= 0) {
160                         // cache directive value
161                         char *arg = *++argv;
162                         // OFF -> 0, anything else -> 1
163                         bool onoff = (0 != strcmp("OFF", arg));
164                         // 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                                 llist_t *l;
179                                 // remove the string from abort conditions
180                                 // N.B. gotta refresh maximum length too...
181 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
182                                 max_abort_len = 0;
183 # endif
184                                 for (l = aborts; l; l = l->link) {
185 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
186                                         size_t len = strlen(l->data);
187 # endif
188                                         if (strcmp(arg, l->data) == 0) {
189                                                 llist_unlink(&aborts, l);
190                                                 continue;
191                                         }
192 # if ENABLE_FEATURE_CHAT_VAR_ABORT_LEN
193                                         if (len > max_abort_len)
194                                                 max_abort_len = len;
195 # endif
196                                 }
197 #endif
198                         } else if (DIR_TIMEOUT == key) {
199                                 // set new timeout
200                                 // -1 means OFF
201                                 timeout = atoi(arg) * 1000;
202                                 // 0 means default
203                                 // >0 means value in msecs
204                                 if (!timeout)
205                                         timeout = DEFAULT_CHAT_TIMEOUT;
206                         } else if (DIR_ECHO == key) {
207                                 // turn echo on/off
208                                 // N.B. echo means dumping device input/output to stderr
209                                 echo = onoff;
210                         } else if (DIR_RECORD == key) {
211                                 // turn record on/off
212                                 // N.B. record means dumping device input to a file
213                                         // close previous record_fd
214                                 if (record_fd > 0)
215                                         close(record_fd);
216                                 // N.B. do we have to die here on open error?
217                                 record_fd = (onoff) ? xopen(arg, O_WRONLY|O_CREAT|O_TRUNC) : -1;
218                         } else if (DIR_SAY == key) {
219                                 // just print argument verbatim
220                                 // TODO: should we use full_write() to avoid unistd/stdio conflict?
221                                 bb_error_msg("%s", arg);
222                         }
223                         // next, please!
224                         argv++;
225                 // ordinary expect-send pair!
226                 } else {
227                         //-----------------------
228                         // do expect
229                         //-----------------------
230                         int expect_len;
231                         size_t buf_len = 0;
232                         size_t max_len = max_abort_len;
233
234                         struct pollfd pfd;
235 #if ENABLE_FEATURE_CHAT_NOFAIL
236                         int nofail = 0;
237 #endif
238                         char *expect = *argv++;
239
240                         // sanity check: shall we really expect something?
241                         if (!expect)
242                                 goto expect_done;
243
244 #if ENABLE_FEATURE_CHAT_NOFAIL
245                         // if expect starts with -
246                         if ('-' == *expect) {
247                                 // swallow -
248                                 expect++;
249                                 // and enter nofail mode
250                                 nofail++;
251                         }
252 #endif
253
254 #ifdef ___TEST___BUF___ // test behaviour with a small buffer
255 #       undef COMMON_BUFSIZE
256 #       define COMMON_BUFSIZE 6
257 #endif
258                         // expand escape sequences in expect
259                         expect_len = unescape(expect, &expect_len /*dummy*/);
260                         if (expect_len > max_len)
261                                 max_len = expect_len;
262                         // sanity check:
263                         // we should expect more than nothing but not more than input buffer
264                         // TODO: later we'll get rid of fixed-size buffer
265                         if (!expect_len)
266                                 goto expect_done;
267                         if (max_len >= COMMON_BUFSIZE) {
268                                 exitcode = ERR_MEM;
269                                 goto expect_done;
270                         }
271
272                         // get reply
273                         pfd.fd = STDIN_FILENO;
274                         pfd.events = POLLIN;
275                         while (!exitcode
276                             && poll(&pfd, 1, timeout) > 0
277                             && (pfd.revents & POLLIN)
278                         ) {
279 #define buf bb_common_bufsiz1
280                                 llist_t *l;
281                                 ssize_t delta;
282
283                                 // read next char from device
284                                 if (safe_read(STDIN_FILENO, buf+buf_len, 1) > 0) {
285                                         // dump device input if RECORD fname
286                                         if (record_fd > 0) {
287                                                 full_write(record_fd, buf+buf_len, 1);
288                                         }
289                                         // dump device input if ECHO ON
290                                         if (echo > 0) {
291 //                                              if (buf[buf_len] < ' ') {
292 //                                                      full_write(STDERR_FILENO, "^", 1);
293 //                                                      buf[buf_len] += '@';
294 //                                              }
295                                                 full_write(STDERR_FILENO, buf+buf_len, 1);
296                                         }
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
319                                 delta = buf_len - expect_len;
320                                 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len))
321                                         goto expect_done;
322 #undef buf
323                         } /* while (have data) */
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);
366                                         buf = loaded = xmalloc_xopen_read_close(buf, NULL);
367                                 }
368                                 // expand escape sequences in command
369                                 len = unescape(buf, &nocr);
370
371                                 // send command
372                                 alarm(timeout);
373                                 pfd.fd = STDOUT_FILENO;
374                                 pfd.events = POLLOUT;
375                                 while (len && !exitcode
376                                     && poll(&pfd, 1, -1) > 0
377                                     && (pfd.revents & POLLOUT)
378                                 ) {
379 #if ENABLE_FEATURE_CHAT_SEND_ESCAPES
380                                         // "\\d" means 1 sec delay, "\\p" means 0.01 sec delay
381                                         // "\\K" means send BREAK
382                                         char c = *buf;
383                                         if ('\\' == c) {
384                                                 c = *++buf;
385                                                 if ('d' == c) {
386                                                         sleep(1);
387                                                         len--;
388                                                         continue;
389                                                 }
390                                                 if ('p' == c) {
391                                                         usleep(10000);
392                                                         len--;
393                                                         continue;
394                                                 }
395                                                 if ('K' == c) {
396                                                         tcsendbreak(STDOUT_FILENO, 0);
397                                                         len--;
398                                                         continue;
399                                                 }
400                                                 buf--;
401                                         }
402                                         if (safe_write(STDOUT_FILENO, buf, 1) != 1)
403                                                 break;
404                                         len--;
405                                         buf++;
406 #else
407                                         len -= full_write(STDOUT_FILENO, buf, len);
408 #endif
409                                 } /* while (can write) */
410                                 alarm(0);
411
412                                 // report I/O error if there still exists at least one non-sent char
413                                 if (len)
414                                         exitcode = ERR_IO;
415
416                                 // free loaded command (if any)
417                                 if (loaded)
418                                         free(loaded);
419 #if ENABLE_FEATURE_CHAT_IMPLICIT_CR
420                                 // or terminate command with \r (if not inhibited)
421                                 else if (!nocr)
422                                         xwrite(STDOUT_FILENO, "\r", 1);
423 #endif
424                                 // bail out unless we sent command successfully
425                                 if (exitcode)
426                                         break;
427                         } /* if (*argv) */
428                 }
429         } /* while (*argv) */
430
431 #if ENABLE_FEATURE_CHAT_TTY_HIFI
432         tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0);
433 #endif
434
435         return exitcode;
436 }