ae471a3fa2c6913874e07cb9f9cf6b711baedb67
[oweals/busybox.git] / miscutils / conspy.c
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  * example :    conspy num              shared access to console num
13  * or           conspy -d num           screenshot of console num
14  * or           conspy -cs num          poor man's GNU screen like
15  */
16
17 //applet:IF_CONSPY(APPLET(conspy, _BB_DIR_BIN, _BB_SUID_DROP))
18
19 //kbuild:lib-$(CONFIG_CONSPY) += conspy.o
20
21 //config:config CONSPY
22 //config:       bool "conspy"
23 //config:       default n
24 //config:       help
25 //config:         A text-mode VNC like program for Linux virtual terminals.
26 //config:         example : conspy num      shared access to console num
27 //config:         or        conspy -d num   screenshot of console num
28 //config:         or        conspy -cs num  poor man's GNU screen like
29
30 //usage:#define conspy_trivial_usage
31 //usage:     "[-vcsndf] [-x ROW] [-y LINE] [CONSOLE_NO]"
32 //usage:#define conspy_full_usage "\n\n"
33 //usage:     "A text-mode VNC like program for Linux virtual consoles."
34 //usage:     "\nTo exit, quickly press ESC 3 times."
35 //usage:     "\n"
36 //usage:     "\nOptions:"
37 //usage:     "\n        -v      Don't send keystrokes to the console"
38 //usage:     "\n        -c      Create missing devices in /dev"
39 //usage:     "\n        -s      Open a SHELL session"
40 //usage:     "\n        -n      Black & white"
41 //usage:     "\n        -d      Dump console to stdout"
42 //usage:     "\n        -f      Follow cursor"
43 //usage:     "\n        -x ROW  Starting row"
44 //usage:     "\n        -y LINE Starting line"
45
46 #include "libbb.h"
47 #include <sys/kd.h>
48
49 struct screen_info {
50         unsigned char lines, rows, cursor_x, cursor_y;
51 };
52
53 #define CHAR(x) ((x)[0])
54 #define ATTR(x) ((x)[1])
55 #define NEXT(x) ((x)+=2)
56 #define DATA(x) (* (short *) (x))
57
58 struct globals {
59         char* data;
60         int size;
61         int x, y;
62         int kbd_fd;
63         unsigned width;
64         unsigned height;
65         char last_attr;
66         struct screen_info info;
67         struct termios term_orig;
68 };
69
70 #define G (*ptr_to_globals)
71 #define INIT_G() do { \
72         SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
73 } while (0)
74
75 enum {
76         FLAG_v,  // view only
77         FLAG_c,  // create device if need
78         FLAG_s,  // session
79         FLAG_n,  // no colors
80         FLAG_d,  // dump screen
81         FLAG_f,  // follow cursor
82 };
83 #define FLAG(x) (1 << FLAG_##x)
84 #define BW (option_mask32 & FLAG(n))
85
86 static void screen_read_close(int fd, char *data)
87 {
88         unsigned i, j;
89
90         xread(fd, data, G.size);
91         G.last_attr = 0;
92         for (i = 0; i < G.info.lines; i++) {
93                 for (j = 0; j < G.info.rows; j++, NEXT(data)) {
94                         unsigned x = j - G.x; // if will catch j < G.x too
95                         unsigned y = i - G.y; // if will catch i < G.y too
96
97                         if (CHAR(data) < ' ')
98                                 CHAR(data) = ' ';
99                         if (y >= G.height || x >= G.width)
100                                 DATA(data) = 0;
101                 }
102         }
103         close(fd);
104 }
105
106 static void screen_char(char *data)
107 {
108         if (!BW && G.last_attr != ATTR(data)) {
109                 //                            BLGCRMOW
110                 static const char color[8] = "04261537";
111
112                 printf("\033[%c;4%c;3%cm",
113                         (ATTR(data) & 8) ? '1'  // bold
114                                          : '0', // defaults
115                         color[(ATTR(data) >> 4) & 7], color[ATTR(data) & 7]);
116                 G.last_attr = ATTR(data);
117         }
118         bb_putchar(CHAR(data));
119 }
120
121 #define clrscr()  printf("\033[1;1H" "\033[0J")
122 #define curoff()  printf("\033[?25l")
123
124 static void curon(void)
125 {
126         printf("\033[?25h");
127 }
128
129 static void gotoxy(int row, int line)
130 {
131         printf("\033[%u;%uH", line + 1, row + 1);
132 }
133
134 static void screen_dump(char *data)
135 {
136         int linefeed_cnt;
137         int line, row;
138         int linecnt = G.info.lines - G.y;
139
140         data += 2 * G.y * G.info.rows;
141         linefeed_cnt = 0;
142         for (line = 0; line < linecnt && line < G.height; line++) {
143                 int space_cnt = 0;
144                 for (row = 0; row < G.info.rows; row++, NEXT(data)) {
145                         unsigned tty_row = row - G.x; // if will catch row < G.x too
146
147                         if (tty_row >= G.width)
148                                 continue;
149                         space_cnt++;
150                         if (BW && (CHAR(data) | ' ') == ' ')
151                                 continue;
152                         while (linefeed_cnt != 0) {
153                                 bb_putchar('\r');
154                                 bb_putchar('\n');
155                                 linefeed_cnt--;
156                         }
157                         while (--space_cnt)
158                                 bb_putchar(' ');
159                         screen_char(data);
160                 }
161                 linefeed_cnt++;
162         }
163 }
164
165 static void curmove(void)
166 {
167         unsigned cx = G.info.cursor_x - G.x;
168         unsigned cy = G.info.cursor_y - G.y;
169
170         if (cx >= G.width || cy >= G.height) {
171                 curoff();
172         } else {
173                 curon();
174                 gotoxy(cx, cy);
175         }
176         fflush_all();
177 }
178
179 static void cleanup(int code)
180 {
181         curon();
182         fflush_all();
183         tcsetattr(G.kbd_fd, TCSANOW, &G.term_orig);
184         if (ENABLE_FEATURE_CLEAN_UP) {
185                 free(ptr_to_globals);
186                 close(G.kbd_fd);
187         }
188         // Reset attributes
189         if (!BW)
190                 printf("\033[0m");
191         bb_putchar('\n');
192         if (code > 1)
193                 kill_myself_with_sig(code); // does not return
194         exit(code);
195 }
196
197 static void get_initial_data(const char* vcsa_name)
198 {
199         int size;
200         int fd = xopen(vcsa_name, O_RDONLY);
201         xread(fd, &G.info, 4);
202         G.size = size = G.info.rows * G.info.lines * 2;
203         G.width = G.height = UINT_MAX;
204         G.data = xzalloc(2 * size);
205         screen_read_close(fd, G.data);
206 }
207
208 static void create_cdev_if_doesnt_exist(const char* name, dev_t dev)
209 {
210         int fd = open(name, O_RDONLY);
211         if (fd != -1)
212                 close(fd);
213         else if (errno == ENOENT)
214                 mknod(name, S_IFCHR | 0660, dev);
215 }
216
217 static NOINLINE void start_shell_in_child(const char* tty_name)
218 {
219         int pid = vfork();
220         if (pid < 0) {
221                 bb_perror_msg_and_die("vfork");
222         }
223         if (pid == 0) {
224                 struct termios termchild;
225                 char *shell = getenv("SHELL");
226
227                 if (!shell)
228                         shell = (char *) DEFAULT_SHELL;
229                 signal(SIGHUP, SIG_IGN);
230                 // set tty as a controlling tty
231                 setsid();
232                 // make tty to be input, output, error
233                 close(0);
234                 xopen(tty_name, O_RDWR); // uses fd 0
235                 xdup2(0, 1);
236                 xdup2(0, 2);
237                 ioctl(0, TIOCSCTTY, 1);
238                 tcsetpgrp(0, getpid());
239                 tcgetattr(0, &termchild);
240                 termchild.c_lflag |= ECHO;
241                 termchild.c_oflag |= ONLCR | XTABS;
242                 termchild.c_iflag |= ICRNL;
243                 termchild.c_iflag &= ~IXOFF;
244                 tcsetattr_stdin_TCSANOW(&termchild);
245                 execl(shell, shell, "-i", (char *) NULL);
246                 bb_simple_perror_msg_and_die(shell);
247         }
248 }
249
250 int conspy_main(int argc UNUSED_PARAM, char **argv) MAIN_EXTERNALLY_VISIBLE;
251 int conspy_main(int argc UNUSED_PARAM, char **argv)
252 {
253         char *buffer[2];
254         char vcsa_name[sizeof("/dev/vcsa") + 2];
255         char tty_name[sizeof("/dev/tty") + 2];
256 #define keybuf bb_common_bufsiz1
257         struct termios termbuf;
258         unsigned opts;
259         unsigned ttynum;
260         int poll_timeout_ms;
261         int current;
262         int ioerror_count;
263         int key_count;
264         int escape_count;
265         int nokeys;
266 #if ENABLE_LONG_OPTS
267         static const char getopt_longopts[] ALIGN1 =
268                 "viewonly\0"     No_argument "v"
269                 "createdevice\0" No_argument "c"
270                 "session\0"      No_argument "s"
271                 "nocolors\0"     No_argument "n"
272                 "dump\0"         No_argument "d"
273                 "follow\0"       No_argument "f"
274                 ;
275
276         applet_long_options = getopt_longopts;
277 #endif
278         INIT_G();
279         strcpy(vcsa_name, "/dev/vcsa");
280
281         opt_complementary = "x+:y+"; // numeric params
282         opts = getopt32(argv, "vcsndfx:y:", &G.x, &G.y);
283         argv += optind;
284         ttynum = 0;
285         if (argv[0]) {
286                 ttynum = xatou_range(argv[0], 0, 63);
287                 sprintf(vcsa_name + sizeof("/dev/vcsa")-1, "%u", ttynum);
288         }
289         sprintf(tty_name, "%s%u", "/dev/tty", ttynum);
290         if (opts & FLAG(c)) {
291                 if ((opts & (FLAG(s)|FLAG(v))) != FLAG(v))
292                         create_cdev_if_doesnt_exist(tty_name, makedev(4, ttynum));
293                 create_cdev_if_doesnt_exist(vcsa_name, makedev(7, 128 + ttynum));
294         }
295         if ((opts & FLAG(s)) && ttynum) {
296                 start_shell_in_child(tty_name);
297         }
298
299         get_initial_data(vcsa_name);
300         G.kbd_fd = xopen(CURRENT_TTY, O_RDONLY);
301         if (opts & FLAG(d)) {
302                 screen_dump(G.data);
303                 bb_putchar('\n');
304                 if (ENABLE_FEATURE_CLEAN_UP) {
305                         free(ptr_to_globals);
306                         close(G.kbd_fd);
307                 }
308                 return 0;
309         }
310
311         bb_signals(BB_FATAL_SIGS, cleanup);
312         // All characters must be passed through to us unaltered
313         tcgetattr(G.kbd_fd, &G.term_orig);
314         termbuf = G.term_orig;
315         termbuf.c_iflag &= ~(BRKINT|INLCR|ICRNL|IXON|IXOFF|IUCLC|IXANY|IMAXBEL);
316         termbuf.c_oflag &= ~(OPOST);
317         termbuf.c_lflag &= ~(ISIG|ICANON|ECHO);
318         termbuf.c_cc[VMIN] = 1;
319         termbuf.c_cc[VTIME] = 0;
320         tcsetattr(G.kbd_fd, TCSANOW, &termbuf);
321         buffer[0] = G.data;
322         buffer[1] = G.data + G.size;
323         poll_timeout_ms = 250;
324         ioerror_count = key_count = escape_count = nokeys = current = 0;
325         while (1) {
326                 struct pollfd pfd;
327                 int vcsa_handle;
328                 int bytes_read;
329                 int i, j;
330                 int next = 1 - current;
331                 char *data = buffer[next];
332                 char *old  = buffer[current];
333
334                 // Close & re-open vcsa in case they have
335                 // swapped virtual consoles
336                 vcsa_handle = xopen(vcsa_name, O_RDONLY);
337                 xread(vcsa_handle, &G.info, 4);
338                 if (G.size != (G.info.rows * G.info.lines * 2)) {
339                         cleanup(1);
340                 }
341                 i = G.width;
342                 j = G.height;
343                 get_terminal_width_height(G.kbd_fd, &G.width, &G.height);
344                 if ((option_mask32 & FLAG(f))) {
345                         int nx = G.info.cursor_x - G.width + 1;
346                         int ny = G.info.cursor_y - G.height + 1;
347
348                         if (G.info.cursor_x < G.x) {
349                                 G.x = G.info.cursor_x;
350                                 i = 0;  // force refresh
351                         }
352                         if (nx > G.x) {
353                                 G.x = nx;
354                                 i = 0;  // force refresh
355                         }
356                         if (G.info.cursor_y < G.y) {
357                                 G.y = G.info.cursor_y;
358                                 i = 0;  // force refresh
359                         }
360                         if (ny > G.y) {
361                                 G.y = ny;
362                                 i = 0;  // force refresh
363                         }
364                 }
365
366                 // Scan console data and redraw our tty where needed
367                 screen_read_close(vcsa_handle, data);
368                 if (i != G.width || j != G.height) {
369                         clrscr();
370                         screen_dump(data);
371                 }
372                 else for (i = 0; i < G.info.lines; i++) {
373                         char *last = last;
374                         char *first = NULL;
375                         int iy = i - G.y;
376
377                         if (iy >= (int) G.height)
378                                 break;
379                         for (j = 0; j < G.info.rows; j++) {
380                                 last = data;
381                                 if (DATA(data) != DATA(old) && iy >= 0) {
382                                         unsigned jx = j - G.x;
383
384                                         last = NULL;
385                                         if (first == NULL && jx < G.width) {
386                                                 first = data;
387                                                 gotoxy(jx, iy);
388                                         }
389                                 }
390                                 NEXT(old);
391                                 NEXT(data);
392                         }
393                         if (first == NULL)
394                                 continue;
395                         if (last == NULL)
396                                 last = data;
397
398                         // Write the data to the screen
399                         for (; first < last; NEXT(first))
400                                 screen_char(first);
401                 }
402                 current = next;
403                 curmove();
404
405                 // Wait for local user keypresses
406                 pfd.fd = G.kbd_fd;
407                 pfd.events = POLLIN;
408                 bytes_read = 0;
409                 switch (poll(&pfd, 1, poll_timeout_ms)) {
410                 case -1:
411                         if (errno != EINTR)
412                                 cleanup(1);
413                         break;
414                 case 0:
415                         if (++nokeys >= 4)
416                                 nokeys = escape_count = 0;
417                         break;
418                 default:
419                         // Read the keys pressed
420                         bytes_read = read(G.kbd_fd, keybuf + key_count,
421                                         sizeof(keybuf) - key_count);
422                         if (bytes_read < 0)
423                                 cleanup(1);
424
425                         // Do exit processing
426                         for (i = 0; i < bytes_read; i++) {
427                                 if (keybuf[key_count + i] != '\033')
428                                         escape_count = 0;
429                                 else if (++escape_count >= 3)
430                                         cleanup(0);
431                         }
432                 }
433                 poll_timeout_ms = 250;
434
435                 // Insert all keys pressed into the virtual console's input
436                 // buffer.  Don't do this if the virtual console is in scan
437                 // code mode - giving ASCII characters to a program expecting
438                 // scan codes will confuse it.
439                 if (!(option_mask32 & FLAG(v)) && escape_count == 0) {
440                         int handle, result;
441                         long kbd_mode;
442
443                         key_count += bytes_read;
444                         handle = xopen(tty_name, O_WRONLY);
445                         result = ioctl(handle, KDGKBMODE, &kbd_mode);
446                         if (result == -1)
447                                 /* nothing */;
448                         else if (kbd_mode != K_XLATE && kbd_mode != K_UNICODE)
449                                 key_count = 0; // scan code mode
450                         else {
451                                 for (i = 0; i < key_count && result != -1; i++)
452                                         result = ioctl(handle, TIOCSTI, keybuf + i);
453                                 key_count -= i;
454                                 if (key_count)
455                                         memmove(keybuf, keybuf + i, key_count);
456                                 // If there is an application on console which reacts
457                                 // to keypresses, we need to make our first sleep
458                                 // shorter to quickly redraw whatever it printed there.
459                                 poll_timeout_ms = 20;
460                         }
461                         // Close & re-open tty in case they have
462                         // swapped virtual consoles
463                         close(handle);
464
465                         // We sometimes get spurious IO errors on the TTY
466                         // as programs close and re-open it
467                         if (result != -1)
468                                 ioerror_count = 0;
469                         else if (errno != EIO || ++ioerror_count > 4)
470                                 cleanup(1);
471                 }
472         }
473 }