1 /* vi: set sw=4 ts=4: */
3 * A text-mode VNC like program for Linux virtual terminals.
5 * pascal.bellard@ads-lu.com
7 * Based on Russell Stuart's conspy.c
8 * http://ace-host.stuart.id.au/russell/files/conspy.c
10 * Licensed under GPLv2 or later, see file LICENSE in this source tree.
13 //applet:IF_CONSPY(APPLET(conspy, BB_DIR_BIN, BB_SUID_DROP))
15 //kbuild:lib-$(CONFIG_CONSPY) += conspy.o
17 //config:config CONSPY
18 //config: bool "conspy"
20 //config: select PLATFORM_LINUX
22 //config: A text-mode VNC like program for Linux virtual terminals.
23 //config: example: conspy NUM shared access to console num
24 //config: or conspy -nd NUM screenshot of console num
25 //config: or conspy -cs NUM poor man's GNU screen like
27 //usage:#define conspy_trivial_usage
28 //usage: "[-vcsndfFQ] [-x COL] [-y LINE] [CONSOLE_NO]"
29 //usage:#define conspy_full_usage "\n\n"
30 //usage: "A text-mode VNC like program for Linux virtual consoles."
31 //usage: "\nTo exit, quickly press ESC 3 times."
33 //usage: "\n -v Don't send keystrokes to the console"
34 //usage: "\n -c Create missing /dev/{tty,vcsa}N"
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 -F Assume console is on a framebuffer device"
40 //usage: "\n -Q Disable exit on ESC-ESC-ESC"
41 //usage: "\n -x COL Starting column"
42 //usage: "\n -y LINE Starting line"
45 #include "common_bufsiz.h"
52 #define DEV_TTY "/dev/tty"
53 #define DEV_VCSA "/dev/vcsa"
56 unsigned char lines
, cols
, cursor_x
, cursor_y
;
59 #define CHAR(x) (*(uint8_t*)(x))
60 #define ATTR(x) (((uint8_t*)(x))[1])
61 #define NEXT(x) ((x) += 2)
62 #define DATA(x) (*(uint16_t*)(x))
74 int first_line_offset
;
76 // cached local tty parameters
81 smallint curoff
; // unknown:0 cursor on:-1 cursor off:1
82 char attrbuf
[sizeof("0;1;5;30;40m")];
84 struct screen_info remote
;
85 // saved local tty terminfo
86 struct termios term_orig
;
87 char vcsa_name
[sizeof(DEV_VCSA
"NN")];
90 #define G (*ptr_to_globals)
91 #define INIT_G() do { \
92 SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
93 G.width = G.height = UINT_MAX; \
99 FLAG_c
, // create device if need
100 FLAG_Q
, // never exit
103 FLAG_d
, // dump screen
104 FLAG_f
, // follow cursor
105 FLAG_F
, // framebuffer
107 #define FLAG(x) (1 << FLAG_##x)
108 #define BW (option_mask32 & FLAG(n))
110 static void putcsi(const char *s
)
112 fputs(ESC
"[", stdout
);
116 static void clrscr(void)
118 // Home, clear till end of screen
119 putcsi("1;1H" ESC
"[J");
123 static void set_cursor(int state
)
125 if (G
.curoff
!= state
) {
128 bb_putchar("h?l"[1 + state
]);
132 static void gotoxy(int col
, int line
)
134 if (G
.col
!= col
|| G
.line
!= line
) {
137 printf(ESC
"[%u;%uH", line
+ 1, col
+ 1);
141 static void cleanup(int code
) NORETURN
;
142 static void cleanup(int code
)
144 set_cursor(CURSOR_ON
);
145 tcsetattr(G
.kbd_fd
, TCSANOW
, &G
.term_orig
);
146 if (ENABLE_FEATURE_CLEAN_UP
) {
153 if (code
> EXIT_FAILURE
)
154 kill_myself_with_sig(code
);
158 static void screen_read_close(void)
164 // Close & re-open vcsa in case they have swapped virtual consoles
165 vcsa_fd
= xopen(G
.vcsa_name
, O_RDONLY
);
166 xread(vcsa_fd
, &G
.remote
, 4);
167 i
= G
.remote
.cols
* 2;
168 G
.first_line_offset
= G
.y
* i
;
170 if (G
.data
== NULL
) {
172 G
.data
= xzalloc(2 * i
);
175 cleanup(EXIT_FAILURE
);
177 data
= G
.data
+ G
.current
;
178 xread(vcsa_fd
, data
, G
.size
);
180 for (i
= 0; i
< G
.remote
.lines
; i
++) {
181 for (j
= 0; j
< G
.remote
.cols
; j
++, NEXT(data
)) {
182 unsigned x
= j
- G
.x
; // if will catch j < G.x too
183 unsigned y
= i
- G
.y
; // if will catch i < G.y too
185 if (y
>= G
.height
|| x
>= G
.width
)
188 uint8_t ch
= CHAR(data
);
190 CHAR(data
) = ch
| 0x40;
198 static void screen_char(char *data
)
202 uint8_t attr
= ATTR(data
);
204 if (option_mask32
& FLAG(F
)) {
207 attr_diff
= G
.last_attr
^ attr
;
209 // Attribute layout for VGA compatible text videobuffer:
215 // 00000000 <- lsb bit on the right
216 // bold text / text 8th bit
220 // TODO: apparently framebuffer-based console uses different layout
221 // (bug? attempt to get 8th text bit in better position?)
226 // 00000000 <- lsb bit on the right
232 // converting RGB color bit triad to BGR:
233 static const char color
[8] = "04261537";
234 const uint8_t fg_mask
= 0x07, bold_mask
= 0x08;
235 const uint8_t bg_mask
= 0x70, blink_mask
= 0x80;
240 // (G.last_attr & ~attr) has 1 only where
241 // G.last_attr has 1 but attr has 0.
242 // Here we check whether we have transition
243 // bold->non-bold or blink->non-blink:
244 if (G
.last_attr
< 0 // initial value
245 || ((G
.last_attr
& ~attr
) & (bold_mask
| blink_mask
)) != 0
247 *ptr
++ = '0'; // "reset all attrs"
249 // must set fg & bg, maybe need to set bold or blink:
250 attr_diff
= attr
| ~(bold_mask
| blink_mask
);
253 if (attr_diff
& bold_mask
) {
257 if (attr_diff
& blink_mask
) {
261 if (attr_diff
& fg_mask
) {
263 *ptr
++ = color
[attr
& fg_mask
];
266 if (attr_diff
& bg_mask
) {
268 *ptr
++ = color
[(attr
& bg_mask
) >> 4];
269 ptr
++; // last attribute
271 if (ptr
!= G
.attrbuf
) {
282 static void screen_dump(void)
286 int linecnt
= G
.remote
.lines
- G
.y
;
287 char *data
= G
.data
+ G
.current
+ G
.first_line_offset
;
290 for (line
= 0; line
< linecnt
&& line
< G
.height
; line
++) {
292 for (col
= 0; col
< G
.remote
.cols
; col
++, NEXT(data
)) {
293 unsigned tty_col
= col
- G
.x
; // if will catch col < G.x too
295 if (tty_col
>= G
.width
)
298 if (BW
&& CHAR(data
) == ' ')
300 while (linefeed_cnt
!= 0) {
301 //bb_putchar('\r'); - tty driver does it for us
313 static void curmove(void)
315 unsigned cx
= G
.remote
.cursor_x
- G
.x
;
316 unsigned cy
= G
.remote
.cursor_y
- G
.y
;
317 int cursor
= CURSOR_OFF
;
319 if (cx
< G
.width
&& cy
< G
.height
) {
326 static void create_cdev_if_doesnt_exist(const char* name
, dev_t dev
)
328 int fd
= open(name
, O_RDONLY
);
331 else if (errno
== ENOENT
)
332 mknod(name
, S_IFCHR
| 0660, dev
);
335 static NOINLINE
void start_shell_in_child(const char* tty_name
)
339 struct termios termchild
;
340 const char *shell
= get_shell_name();
342 signal(SIGHUP
, SIG_IGN
);
343 // set tty as a controlling tty
345 // make tty to be input, output, error
347 xopen(tty_name
, O_RDWR
); // uses fd 0
350 ioctl(0, TIOCSCTTY
, 1);
351 tcsetpgrp(0, getpid());
352 tcgetattr(0, &termchild
);
353 termchild
.c_lflag
|= ECHO
;
354 termchild
.c_oflag
|= ONLCR
| XTABS
;
355 termchild
.c_iflag
|= ICRNL
;
356 termchild
.c_iflag
&= ~IXOFF
;
357 tcsetattr_stdin_TCSANOW(&termchild
);
358 execl(shell
, shell
, "-i", (char *) NULL
);
359 bb_simple_perror_msg_and_die(shell
);
363 int conspy_main(int argc
, char **argv
) MAIN_EXTERNALLY_VISIBLE
;
364 int conspy_main(int argc UNUSED_PARAM
, char **argv
)
366 char tty_name
[sizeof(DEV_TTY
"NN")];
367 struct termios termbuf
;
372 static const char getopt_longopts
[] ALIGN1
=
373 "viewonly\0" No_argument
"v"
374 "createdevice\0" No_argument
"c"
375 "neverquit\0" No_argument
"Q"
376 "session\0" No_argument
"s"
377 "nocolors\0" No_argument
"n"
378 "dump\0" No_argument
"d"
379 "follow\0" No_argument
"f"
380 "framebuffer\0" No_argument
"F"
383 applet_long_options
= getopt_longopts
;
385 #define keybuf bb_common_bufsiz1
386 setup_common_bufsiz();
389 strcpy(G
.vcsa_name
, DEV_VCSA
);
391 opt_complementary
= "x+:y+"; // numeric params
392 opts
= getopt32(argv
, "vcQsndfFx:y:", &G
.x
, &G
.y
);
396 ttynum
= xatou_range(argv
[0], 0, 63);
397 sprintf(G
.vcsa_name
+ sizeof(DEV_VCSA
)-1, "%u", ttynum
);
399 sprintf(tty_name
, "%s%u", DEV_TTY
, ttynum
);
400 if (opts
& FLAG(c
)) {
401 if ((opts
& (FLAG(s
)|FLAG(v
))) != FLAG(v
))
402 create_cdev_if_doesnt_exist(tty_name
, makedev(4, ttynum
));
403 create_cdev_if_doesnt_exist(G
.vcsa_name
, makedev(7, 128 + ttynum
));
405 if ((opts
& FLAG(s
)) && ttynum
) {
406 start_shell_in_child(tty_name
);
410 if (opts
& FLAG(d
)) {
416 bb_signals(BB_FATAL_SIGS
, cleanup
);
418 // All characters must be passed through to us unaltered
419 G
.kbd_fd
= xopen(CURRENT_TTY
, O_RDONLY
);
420 tcgetattr(G
.kbd_fd
, &G
.term_orig
);
421 termbuf
= G
.term_orig
;
422 termbuf
.c_iflag
&= ~(BRKINT
|INLCR
|ICRNL
|IXON
|IXOFF
|IUCLC
|IXANY
|IMAXBEL
);
423 //termbuf.c_oflag &= ~(OPOST); - no, we still want \n -> \r\n
424 termbuf
.c_lflag
&= ~(ISIG
|ICANON
|ECHO
);
425 termbuf
.c_cc
[VMIN
] = 1;
426 termbuf
.c_cc
[VTIME
] = 0;
427 tcsetattr(G
.kbd_fd
, TCSANOW
, &termbuf
);
429 poll_timeout_ms
= 250;
436 // in the first loop G.width = G.height = 0: refresh
439 get_terminal_width_height(G
.kbd_fd
, &G
.width
, &G
.height
);
440 if (option_mask32
& FLAG(f
)) {
441 int nx
= G
.remote
.cursor_x
- G
.width
+ 1;
442 int ny
= G
.remote
.cursor_y
- G
.height
+ 1;
444 if (G
.remote
.cursor_x
< G
.x
) {
445 G
.x
= G
.remote
.cursor_x
;
446 i
= 0; // force refresh
450 i
= 0; // force refresh
452 if (G
.remote
.cursor_y
< G
.y
) {
453 G
.y
= G
.remote
.cursor_y
;
454 i
= 0; // force refresh
458 i
= 0; // force refresh
462 // Scan console data and redraw our tty where needed
463 old
= G
.data
+ G
.current
;
464 G
.current
= G
.size
- G
.current
;
465 data
= G
.data
+ G
.current
;
467 if (i
!= G
.width
|| j
!= G
.height
) {
471 // For each remote line
472 old
+= G
.first_line_offset
;
473 data
+= G
.first_line_offset
;
474 for (i
= G
.y
; i
< G
.remote
.lines
; i
++) {
475 char *first
= NULL
; // first char which needs updating
476 char *last
= last
; // last char which needs updating
477 unsigned iy
= i
- G
.y
;
481 for (j
= 0; j
< G
.remote
.cols
; j
++, NEXT(old
), NEXT(data
)) {
482 unsigned jx
= j
- G
.x
; // if will catch j >= G.x too
484 if (jx
< G
.width
&& DATA(data
) != DATA(old
)) {
493 // Rewrite updated data on the local screen
494 for (; first
<= last
; NEXT(first
))
501 // Wait for local user keypresses
506 switch (poll(&pfd
, 1, poll_timeout_ms
)) {
514 G
.nokeys
= G
.escape_count
= 0;
517 // Read the keys pressed
518 k
= keybuf
+ G
.key_count
;
519 bytes_read
= read(G
.kbd_fd
, k
, COMMON_BUFSIZE
- G
.key_count
);
523 // Do exit processing
524 if (!(option_mask32
& FLAG(Q
))) {
525 for (i
= 0; i
< bytes_read
; i
++) {
528 if (++G
.escape_count
>= 3)
529 cleanup(EXIT_SUCCESS
);
533 poll_timeout_ms
= 250;
534 if (option_mask32
& FLAG(v
)) continue;
536 // Insert all keys pressed into the virtual console's input
537 // buffer. Don't do this if the virtual console is in scan
538 // code mode - giving ASCII characters to a program expecting
539 // scan codes will confuse it.
540 G
.key_count
+= bytes_read
;
541 if (G
.escape_count
== 0) {
545 handle
= xopen(tty_name
, O_WRONLY
);
546 result
= ioctl(handle
, KDGKBMODE
, &kbd_mode
);
551 if (kbd_mode
!= K_XLATE
&& kbd_mode
!= K_UNICODE
) {
552 G
.key_count
= 0; // scan code mode
554 for (; G
.key_count
!= 0; p
++, G
.key_count
--) {
555 result
= ioctl(handle
, TIOCSTI
, p
);
557 memmove(keybuf
, p
, G
.key_count
);
560 // If there is an application on console which reacts
561 // to keypresses, we need to make our first sleep
562 // shorter to quickly redraw whatever it printed there.
563 poll_timeout_ms
= 20;
566 // We sometimes get spurious IO errors on the TTY
567 // as programs close and re-open it
568 else if (errno
!= EIO
|| ++G
.ioerror_count
> 4) {
569 if (ENABLE_FEATURE_CLEAN_UP
)
573 // Close & re-open tty in case they have
574 // swapped virtual consoles
579 cleanup(EXIT_FAILURE
);