diff options
Diffstat (limited to 'miscutils/conspy.c')
-rw-r--r-- | miscutils/conspy.c | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/miscutils/conspy.c b/miscutils/conspy.c new file mode 100644 index 000000000..509a0f271 --- /dev/null +++ b/miscutils/conspy.c | |||
@@ -0,0 +1,547 @@ | |||
1 | /* vi: set sw=4 ts=4: */ | ||
2 | /* | ||
3 | * A text-mode VNC like program for Linux virtual terminals. | ||
4 | * | ||
5 | * pascal.bellard@ads-lu.com | ||
6 | * | ||
7 | * Based on Russell Stuart's conspy.c | ||
8 | * http://ace-host.stuart.id.au/russell/files/conspy.c | ||
9 | * | ||
10 | * Licensed under GPLv2 or later, see file License in this tarball for details. | ||
11 | */ | ||
12 | |||
13 | //applet:IF_CONSPY(APPLET(conspy, _BB_DIR_BIN, _BB_SUID_DROP)) | ||
14 | |||
15 | //kbuild:lib-$(CONFIG_CONSPY) += conspy.o | ||
16 | |||
17 | //config:config CONSPY | ||
18 | //config: bool "conspy" | ||
19 | //config: default n | ||
20 | //config: help | ||
21 | //config: A text-mode VNC like program for Linux virtual terminals. | ||
22 | //config: example: conspy NUM shared access to console num | ||
23 | //config: or conspy -nd NUM screenshot of console num | ||
24 | //config: or conspy -cs NUM poor man's GNU screen like | ||
25 | |||
26 | //usage:#define conspy_trivial_usage | ||
27 | //usage: "[-vcsndf] [-x COL] [-y LINE] [CONSOLE_NO]" | ||
28 | //usage:#define conspy_full_usage "\n\n" | ||
29 | //usage: "A text-mode VNC like program for Linux virtual consoles." | ||
30 | //usage: "\nTo exit, quickly press ESC 3 times." | ||
31 | //usage: "\n" | ||
32 | //usage: "\nOptions:" | ||
33 | //usage: "\n -v Don't send keystrokes to the console" | ||
34 | //usage: "\n -c Create missing devices in /dev" | ||
35 | //usage: "\n -s Open a SHELL session" | ||
36 | //usage: "\n -n Black & white" | ||
37 | //usage: "\n -d Dump console to stdout" | ||
38 | //usage: "\n -f Follow cursor" | ||
39 | //usage: "\n -x COL Starting column" | ||
40 | //usage: "\n -y LINE Starting line" | ||
41 | |||
42 | #include "libbb.h" | ||
43 | #include <sys/kd.h> | ||
44 | |||
45 | struct screen_info { | ||
46 | unsigned char lines, cols, cursor_x, cursor_y; | ||
47 | }; | ||
48 | |||
49 | #define CHAR(x) (*(uint8_t*)(x)) | ||
50 | #define ATTR(x) (((uint8_t*)(x))[1]) | ||
51 | #define NEXT(x) ((x) += 2) | ||
52 | #define DATA(x) (*(uint16_t*)(x)) | ||
53 | |||
54 | struct globals { | ||
55 | char* data; | ||
56 | int size; | ||
57 | int x, y; | ||
58 | int kbd_fd; | ||
59 | int ioerror_count; | ||
60 | int key_count; | ||
61 | int escape_count; | ||
62 | int nokeys; | ||
63 | int current; | ||
64 | int first_line_offset; | ||
65 | int last_attr; | ||
66 | // cached local tty parameters | ||
67 | unsigned width; | ||
68 | unsigned height; | ||
69 | unsigned col; | ||
70 | unsigned line; | ||
71 | smallint curoff; // unknown:0 cursor on:-1 cursor off:1 | ||
72 | char attrbuf[sizeof("\033[0;1;5;30;40m")]; | ||
73 | // remote console | ||
74 | struct screen_info remote; | ||
75 | // saved local tty terminfo | ||
76 | struct termios term_orig; | ||
77 | char vcsa_name[sizeof("/dev/vcsaNN")]; | ||
78 | }; | ||
79 | |||
80 | #define G (*ptr_to_globals) | ||
81 | #define INIT_G() do { \ | ||
82 | SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ | ||
83 | G.attrbuf[0] = '\033'; \ | ||
84 | G.attrbuf[1] = '['; \ | ||
85 | G.width = G.height = UINT_MAX; \ | ||
86 | G.last_attr--; \ | ||
87 | } while (0) | ||
88 | |||
89 | enum { | ||
90 | FLAG_v, // view only | ||
91 | FLAG_c, // create device if need | ||
92 | FLAG_s, // session | ||
93 | FLAG_n, // no colors | ||
94 | FLAG_d, // dump screen | ||
95 | FLAG_f, // follow cursor | ||
96 | }; | ||
97 | #define FLAG(x) (1 << FLAG_##x) | ||
98 | #define BW (option_mask32 & FLAG(n)) | ||
99 | |||
100 | static void clrscr(void) | ||
101 | { | ||
102 | // Home, clear till end of screen | ||
103 | fputs("\033[1;1H" "\033[J", stdout); | ||
104 | G.col = G.line = 0; | ||
105 | } | ||
106 | |||
107 | static void set_cursor(int state) | ||
108 | { | ||
109 | if (G.curoff != state) { | ||
110 | G.curoff = state; | ||
111 | fputs("\033[?25", stdout); | ||
112 | bb_putchar("h?l"[1 + state]); | ||
113 | } | ||
114 | } | ||
115 | |||
116 | static void gotoxy(int col, int line) | ||
117 | { | ||
118 | if (G.col != col || G.line != line) { | ||
119 | G.col = col; | ||
120 | G.line = line; | ||
121 | printf("\033[%u;%uH", line + 1, col + 1); | ||
122 | } | ||
123 | } | ||
124 | |||
125 | static void cleanup(int code) | ||
126 | { | ||
127 | set_cursor(-1); // cursor on | ||
128 | tcsetattr(G.kbd_fd, TCSANOW, &G.term_orig); | ||
129 | if (ENABLE_FEATURE_CLEAN_UP) { | ||
130 | close(G.kbd_fd); | ||
131 | } | ||
132 | // Reset attributes | ||
133 | if (!BW) | ||
134 | fputs("\033[0m", stdout); | ||
135 | bb_putchar('\n'); | ||
136 | if (code > 1) | ||
137 | kill_myself_with_sig(code); | ||
138 | exit(code); | ||
139 | } | ||
140 | |||
141 | static void screen_read_close(void) | ||
142 | { | ||
143 | unsigned i, j; | ||
144 | int vcsa_fd; | ||
145 | char *data; | ||
146 | |||
147 | // Close & re-open vcsa in case they have swapped virtual consoles | ||
148 | vcsa_fd = xopen(G.vcsa_name, O_RDONLY); | ||
149 | xread(vcsa_fd, &G.remote, 4); | ||
150 | i = G.remote.cols * 2; | ||
151 | G.first_line_offset = G.y * i; | ||
152 | i *= G.remote.lines; | ||
153 | if (G.data == NULL) { | ||
154 | G.size = i; | ||
155 | G.data = xzalloc(2 * i); | ||
156 | } | ||
157 | else if (G.size != i) { | ||
158 | cleanup(1); | ||
159 | } | ||
160 | data = G.data + G.current; | ||
161 | xread(vcsa_fd, data, G.size); | ||
162 | close(vcsa_fd); | ||
163 | for (i = 0; i < G.remote.lines; i++) { | ||
164 | for (j = 0; j < G.remote.cols; j++, NEXT(data)) { | ||
165 | unsigned x = j - G.x; // if will catch j < G.x too | ||
166 | unsigned y = i - G.y; // if will catch i < G.y too | ||
167 | |||
168 | if (CHAR(data) < ' ') | ||
169 | CHAR(data) = ' '; | ||
170 | if (y >= G.height || x >= G.width) | ||
171 | DATA(data) = 0; | ||
172 | } | ||
173 | } | ||
174 | } | ||
175 | |||
176 | static void screen_char(char *data) | ||
177 | { | ||
178 | if (!BW) { | ||
179 | uint8_t attr = ATTR(data); | ||
180 | //uint8_t attr = ATTR(data) >> 1; // for framebuffer console | ||
181 | uint8_t attr_diff = G.last_attr ^ attr; | ||
182 | |||
183 | if (attr_diff) { | ||
184 | // Attribute layout for VGA compatible text videobuffer: | ||
185 | // blinking text | ||
186 | // |red bkgd | ||
187 | // ||green bkgd | ||
188 | // |||blue bkgd | ||
189 | // vvvv | ||
190 | // 00000000 <- lsb bit on the right | ||
191 | // bold text / text 8th bit | ||
192 | // red text | ||
193 | // green text | ||
194 | // blue text | ||
195 | // TODO: apparently framebuffer-based console uses different layout | ||
196 | // (bug? attempt to get 8th text bit in better position?) | ||
197 | // red bkgd | ||
198 | // |green bkgd | ||
199 | // ||blue bkgd | ||
200 | // vvv | ||
201 | // 00000000 <- lsb bit on the right | ||
202 | // bold text | ||
203 | // red text | ||
204 | // green text | ||
205 | // blue text | ||
206 | // text 8th bit | ||
207 | // converting RGB color bit triad to BGR: | ||
208 | static const char color[8] = "04261537"; | ||
209 | const uint8_t fg_mask = 0x07, bold_mask = 0x08; | ||
210 | const uint8_t bg_mask = 0x70, blink_mask = 0x80; | ||
211 | char *ptr; | ||
212 | |||
213 | ptr = G.attrbuf + 2; // skip "ESC [" | ||
214 | |||
215 | // (G.last_attr & ~attr) has 1 only where | ||
216 | // G.last_attr has 1 but attr has 0. | ||
217 | // Here we check whether we have transition | ||
218 | // bold->non-bold or blink->non-blink: | ||
219 | if (G.last_attr < 0 // initial value | ||
220 | || ((G.last_attr & ~attr) & (bold_mask | blink_mask)) != 0 | ||
221 | ) { | ||
222 | *ptr++ = '0'; // "reset all attrs" | ||
223 | *ptr++ = ';'; | ||
224 | // must set fg & bg, maybe need to set bold or blink: | ||
225 | attr_diff = attr | ~(bold_mask | blink_mask); | ||
226 | } | ||
227 | G.last_attr = attr; | ||
228 | if (attr_diff & bold_mask) { | ||
229 | *ptr++ = '1'; | ||
230 | *ptr++ = ';'; | ||
231 | } | ||
232 | if (attr_diff & blink_mask) { | ||
233 | *ptr++ = '5'; | ||
234 | *ptr++ = ';'; | ||
235 | } | ||
236 | if (attr_diff & fg_mask) { | ||
237 | *ptr++ = '3'; | ||
238 | *ptr++ = color[attr & fg_mask]; | ||
239 | *ptr++ = ';'; | ||
240 | } | ||
241 | if (attr_diff & bg_mask) { | ||
242 | *ptr++ = '4'; | ||
243 | *ptr++ = color[(attr & bg_mask) >> 4]; | ||
244 | *ptr++ = ';'; | ||
245 | } | ||
246 | if (ptr != G.attrbuf + 2) { | ||
247 | ptr[-1] = 'm'; | ||
248 | *ptr = '\0'; | ||
249 | fputs(G.attrbuf, stdout); | ||
250 | } | ||
251 | } | ||
252 | } | ||
253 | putchar(CHAR(data)); | ||
254 | G.col++; | ||
255 | } | ||
256 | |||
257 | static void screen_dump(void) | ||
258 | { | ||
259 | int linefeed_cnt; | ||
260 | int line, col; | ||
261 | int linecnt = G.remote.lines - G.y; | ||
262 | char *data = G.data + G.current + G.first_line_offset; | ||
263 | |||
264 | linefeed_cnt = 0; | ||
265 | for (line = 0; line < linecnt && line < G.height; line++) { | ||
266 | int space_cnt = 0; | ||
267 | for (col = 0; col < G.remote.cols; col++, NEXT(data)) { | ||
268 | unsigned tty_col = col - G.x; // if will catch col < G.x too | ||
269 | |||
270 | if (tty_col >= G.width) | ||
271 | continue; | ||
272 | space_cnt++; | ||
273 | if (BW && CHAR(data) == ' ') | ||
274 | continue; | ||
275 | while (linefeed_cnt != 0) { | ||
276 | //bb_putchar('\r'); - tty driver does it for us | ||
277 | bb_putchar('\n'); | ||
278 | linefeed_cnt--; | ||
279 | } | ||
280 | while (--space_cnt) | ||
281 | bb_putchar(' '); | ||
282 | screen_char(data); | ||
283 | } | ||
284 | linefeed_cnt++; | ||
285 | } | ||
286 | } | ||
287 | |||
288 | static void curmove(void) | ||
289 | { | ||
290 | unsigned cx = G.remote.cursor_x - G.x; | ||
291 | unsigned cy = G.remote.cursor_y - G.y; | ||
292 | int cursor = 1; | ||
293 | |||
294 | if (cx < G.width && cy < G.height) { | ||
295 | gotoxy(cx, cy); | ||
296 | cursor = -1; | ||
297 | } | ||
298 | set_cursor(cursor); | ||
299 | } | ||
300 | |||
301 | static void create_cdev_if_doesnt_exist(const char* name, dev_t dev) | ||
302 | { | ||
303 | int fd = open(name, O_RDONLY); | ||
304 | if (fd != -1) | ||
305 | close(fd); | ||
306 | else if (errno == ENOENT) | ||
307 | mknod(name, S_IFCHR | 0660, dev); | ||
308 | } | ||
309 | |||
310 | static NOINLINE void start_shell_in_child(const char* tty_name) | ||
311 | { | ||
312 | int pid = xvfork(); | ||
313 | if (pid == 0) { | ||
314 | struct termios termchild; | ||
315 | char *shell = getenv("SHELL"); | ||
316 | |||
317 | if (!shell) | ||
318 | shell = (char *) DEFAULT_SHELL; | ||
319 | signal(SIGHUP, SIG_IGN); | ||
320 | // set tty as a controlling tty | ||
321 | setsid(); | ||
322 | // make tty to be input, output, error | ||
323 | close(0); | ||
324 | xopen(tty_name, O_RDWR); // uses fd 0 | ||
325 | xdup2(0, 1); | ||
326 | xdup2(0, 2); | ||
327 | ioctl(0, TIOCSCTTY, 1); | ||
328 | tcsetpgrp(0, getpid()); | ||
329 | tcgetattr(0, &termchild); | ||
330 | termchild.c_lflag |= ECHO; | ||
331 | termchild.c_oflag |= ONLCR | XTABS; | ||
332 | termchild.c_iflag |= ICRNL; | ||
333 | termchild.c_iflag &= ~IXOFF; | ||
334 | tcsetattr_stdin_TCSANOW(&termchild); | ||
335 | execl(shell, shell, "-i", (char *) NULL); | ||
336 | bb_simple_perror_msg_and_die(shell); | ||
337 | } | ||
338 | } | ||
339 | |||
340 | int conspy_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; | ||
341 | int conspy_main(int argc UNUSED_PARAM, char **argv) | ||
342 | { | ||
343 | char tty_name[sizeof("/dev/ttyNN")]; | ||
344 | #define keybuf bb_common_bufsiz1 | ||
345 | struct termios termbuf; | ||
346 | unsigned opts; | ||
347 | unsigned ttynum; | ||
348 | int poll_timeout_ms; | ||
349 | #if ENABLE_LONG_OPTS | ||
350 | static const char getopt_longopts[] ALIGN1 = | ||
351 | "viewonly\0" No_argument "v" | ||
352 | "createdevice\0" No_argument "c" | ||
353 | "session\0" No_argument "s" | ||
354 | "nocolors\0" No_argument "n" | ||
355 | "dump\0" No_argument "d" | ||
356 | "follow\0" No_argument "f" | ||
357 | ; | ||
358 | |||
359 | applet_long_options = getopt_longopts; | ||
360 | #endif | ||
361 | INIT_G(); | ||
362 | strcpy(G.vcsa_name, "/dev/vcsa"); | ||
363 | |||
364 | opt_complementary = "x+:y+"; // numeric params | ||
365 | opts = getopt32(argv, "vcsndfx:y:", &G.x, &G.y); | ||
366 | argv += optind; | ||
367 | ttynum = 0; | ||
368 | if (argv[0]) { | ||
369 | ttynum = xatou_range(argv[0], 0, 63); | ||
370 | sprintf(G.vcsa_name + sizeof("/dev/vcsa")-1, "%u", ttynum); | ||
371 | } | ||
372 | sprintf(tty_name, "%s%u", "/dev/tty", ttynum); | ||
373 | if (opts & FLAG(c)) { | ||
374 | if ((opts & (FLAG(s)|FLAG(v))) != FLAG(v)) | ||
375 | create_cdev_if_doesnt_exist(tty_name, makedev(4, ttynum)); | ||
376 | create_cdev_if_doesnt_exist(G.vcsa_name, makedev(7, 128 + ttynum)); | ||
377 | } | ||
378 | if ((opts & FLAG(s)) && ttynum) { | ||
379 | start_shell_in_child(tty_name); | ||
380 | } | ||
381 | |||
382 | screen_read_close(); | ||
383 | if (opts & FLAG(d)) { | ||
384 | screen_dump(); | ||
385 | bb_putchar('\n'); | ||
386 | return 0; | ||
387 | } | ||
388 | |||
389 | bb_signals(BB_FATAL_SIGS, cleanup); | ||
390 | |||
391 | // All characters must be passed through to us unaltered | ||
392 | G.kbd_fd = xopen(CURRENT_TTY, O_RDONLY); | ||
393 | tcgetattr(G.kbd_fd, &G.term_orig); | ||
394 | termbuf = G.term_orig; | ||
395 | termbuf.c_iflag &= ~(BRKINT|INLCR|ICRNL|IXON|IXOFF|IUCLC|IXANY|IMAXBEL); | ||
396 | //termbuf.c_oflag &= ~(OPOST); - no, we still want \n -> \r\n | ||
397 | termbuf.c_lflag &= ~(ISIG|ICANON|ECHO); | ||
398 | termbuf.c_cc[VMIN] = 1; | ||
399 | termbuf.c_cc[VTIME] = 0; | ||
400 | tcsetattr(G.kbd_fd, TCSANOW, &termbuf); | ||
401 | |||
402 | poll_timeout_ms = 250; | ||
403 | while (1) { | ||
404 | struct pollfd pfd; | ||
405 | int bytes_read; | ||
406 | int i, j; | ||
407 | char *data, *old; | ||
408 | |||
409 | // in the first loop G.width = G.height = 0: refresh | ||
410 | i = G.width; | ||
411 | j = G.height; | ||
412 | get_terminal_width_height(G.kbd_fd, &G.width, &G.height); | ||
413 | if (option_mask32 & FLAG(f)) { | ||
414 | int nx = G.remote.cursor_x - G.width + 1; | ||
415 | int ny = G.remote.cursor_y - G.height + 1; | ||
416 | |||
417 | if (G.remote.cursor_x < G.x) { | ||
418 | G.x = G.remote.cursor_x; | ||
419 | i = 0; // force refresh | ||
420 | } | ||
421 | if (nx > G.x) { | ||
422 | G.x = nx; | ||
423 | i = 0; // force refresh | ||
424 | } | ||
425 | if (G.remote.cursor_y < G.y) { | ||
426 | G.y = G.remote.cursor_y; | ||
427 | i = 0; // force refresh | ||
428 | } | ||
429 | if (ny > G.y) { | ||
430 | G.y = ny; | ||
431 | i = 0; // force refresh | ||
432 | } | ||
433 | } | ||
434 | |||
435 | // Scan console data and redraw our tty where needed | ||
436 | old = G.data + G.current; | ||
437 | G.current = G.size - G.current; | ||
438 | data = G.data + G.current; | ||
439 | screen_read_close(); | ||
440 | if (i != G.width || j != G.height) { | ||
441 | clrscr(); | ||
442 | screen_dump(); | ||
443 | } else { | ||
444 | // For each remote line | ||
445 | old += G.first_line_offset; | ||
446 | data += G.first_line_offset; | ||
447 | for (i = G.y; i < G.remote.lines; i++) { | ||
448 | char *first = NULL; // first char which needs updating | ||
449 | char *last = last; // last char which needs updating | ||
450 | unsigned iy = i - G.y; | ||
451 | |||
452 | if (iy >= G.height) | ||
453 | break; | ||
454 | for (j = 0; j < G.remote.cols; j++, NEXT(old), NEXT(data)) { | ||
455 | unsigned jx = j - G.x; // if will catch j >= G.x too | ||
456 | |||
457 | if (jx < G.width && DATA(data) != DATA(old)) { | ||
458 | last = data; | ||
459 | if (!first) { | ||
460 | first = data; | ||
461 | gotoxy(jx, iy); | ||
462 | } | ||
463 | } | ||
464 | } | ||
465 | if (first) { | ||
466 | // Rewrite updated data on the local screen | ||
467 | for (; first <= last; NEXT(first)) | ||
468 | screen_char(first); | ||
469 | } | ||
470 | } | ||
471 | } | ||
472 | curmove(); | ||
473 | |||
474 | // Wait for local user keypresses | ||
475 | fflush_all(); | ||
476 | pfd.fd = G.kbd_fd; | ||
477 | pfd.events = POLLIN; | ||
478 | bytes_read = 0; | ||
479 | switch (poll(&pfd, 1, poll_timeout_ms)) { | ||
480 | char *k; | ||
481 | case -1: | ||
482 | if (errno != EINTR) | ||
483 | cleanup(1); | ||
484 | break; | ||
485 | case 0: | ||
486 | if (++G.nokeys >= 4) | ||
487 | G.nokeys = G.escape_count = 0; | ||
488 | break; | ||
489 | default: | ||
490 | // Read the keys pressed | ||
491 | k = keybuf + G.key_count; | ||
492 | bytes_read = read(G.kbd_fd, k, sizeof(keybuf) - G.key_count); | ||
493 | if (bytes_read < 0) | ||
494 | cleanup(1); | ||
495 | |||
496 | // Do exit processing | ||
497 | for (i = 0; i < bytes_read; i++) { | ||
498 | if (k[i] != '\033') | ||
499 | G.escape_count = 0; | ||
500 | else if (++G.escape_count >= 3) | ||
501 | cleanup(0); | ||
502 | } | ||
503 | } | ||
504 | poll_timeout_ms = 250; | ||
505 | |||
506 | // Insert all keys pressed into the virtual console's input | ||
507 | // buffer. Don't do this if the virtual console is in scan | ||
508 | // code mode - giving ASCII characters to a program expecting | ||
509 | // scan codes will confuse it. | ||
510 | if (!(option_mask32 & FLAG(v)) && G.escape_count == 0) { | ||
511 | int handle, result; | ||
512 | long kbd_mode; | ||
513 | |||
514 | G.key_count += bytes_read; | ||
515 | handle = xopen(tty_name, O_WRONLY); | ||
516 | result = ioctl(handle, KDGKBMODE, &kbd_mode); | ||
517 | if (result >= 0) { | ||
518 | char *p = keybuf; | ||
519 | |||
520 | if (kbd_mode != K_XLATE && kbd_mode != K_UNICODE) { | ||
521 | G.key_count = 0; // scan code mode | ||
522 | } | ||
523 | for (; G.key_count != 0; p++, G.key_count--) { | ||
524 | result = ioctl(handle, TIOCSTI, p); | ||
525 | if (result < 0) { | ||
526 | memmove(keybuf, p, G.key_count); | ||
527 | break; | ||
528 | } | ||
529 | // If there is an application on console which reacts | ||
530 | // to keypresses, we need to make our first sleep | ||
531 | // shorter to quickly redraw whatever it printed there. | ||
532 | poll_timeout_ms = 20; | ||
533 | } | ||
534 | } | ||
535 | // Close & re-open tty in case they have | ||
536 | // swapped virtual consoles | ||
537 | close(handle); | ||
538 | |||
539 | // We sometimes get spurious IO errors on the TTY | ||
540 | // as programs close and re-open it | ||
541 | if (result >= 0) | ||
542 | G.ioerror_count = 0; | ||
543 | else if (errno != EIO || ++G.ioerror_count > 4) | ||
544 | cleanup(1); | ||
545 | } | ||
546 | } /* while (1) */ | ||
547 | } | ||