chat.c 11 KB

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