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
45struct 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
54struct 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
89enum {
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
100static 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
107static 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
116static 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
125static 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
141static 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
176static 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
257static 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
288static 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
301static 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
310static 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
340int conspy_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
341int 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}
548