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 tarball for details. 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 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 30#define exitcode bb_got_signal 31 32// trap for critical signals 33static 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 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 93int chat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; 94int 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 // 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 207 // N.B. echo means dumping device input/output to stderr 208 echo = onoff; 209 } 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; 217 } else if (DIR_SAY == key) { 218 // just print argument verbatim 219 // TODO: should we use full_write() to avoid unistd/stdio conflict? 220 bb_error_msg("%s", arg); 221 } 222 // next, please! 223 argv++; 224 // ordinary expect-send pair! 225 } else { 226 //----------------------- 227 // do expect 228 //----------------------- 229 int expect_len; 230 size_t buf_len = 0; 231 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) { 284 // 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// } 294 full_write(STDERR_FILENO, buf+buf_len, 1); 295 } 296 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 318 delta = buf_len - expect_len; 319 if (delta >= 0 && !memcmp(buf+delta, expect, expect_len)) 320 goto expect_done; 321#undef buf 322 } /* while (have data) */ 323 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); 365 buf = loaded = xmalloc_xopen_read_close(buf, NULL); 366 } 367 // expand escape sequences in command 368 len = unescape(buf, &nocr); 369 370 // send command 371 alarm(timeout); 372 pfd.fd = STDOUT_FILENO; 373 pfd.events = POLLOUT; 374 while (len && !exitcode 375 && poll(&pfd, 1, -1) > 0 376 && (pfd.revents & POLLOUT) 377 ) { 378#if ENABLE_FEATURE_CHAT_SEND_ESCAPES 379 // "\\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; 388 } 389 if ('p' == c) { 390 usleep(10000); 391 len--; 392 continue; 393 } 394 if ('K' == c) { 395 tcsendbreak(STDOUT_FILENO, 0); 396 len--; 397 continue; 398 } 399 buf--; 400 } 401 if (safe_write(STDOUT_FILENO, buf, 1) != 1) 402 break; 403 len--; 404 buf++; 405#else 406 len -= full_write(STDOUT_FILENO, buf, len); 407#endif 408 } /* while (can write) */ 409 alarm(0); 410 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 423 // bail out unless we sent command successfully 424 if (exitcode) 425 break; 426 } /* if (*argv) */ 427 } 428 } /* while (*argv) */ 429 430#if ENABLE_FEATURE_CHAT_TTY_HIFI 431 tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio0); 432#endif 433 434 return exitcode; 435} 436