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"
51 #define DEV_TTY "/dev/tty"
52 #define DEV_VCSA "/dev/vcsa"
55 unsigned char lines
, cols
, cursor_x
, cursor_y
;
58 #define CHAR(x) (*(uint8_t*)(x))
59 #define ATTR(x) (((uint8_t*)(x))[1])
60 #define NEXT(x) ((x) += 2)
61 #define DATA(x) (*(uint16_t*)(x))
73 int first_line_offset
;
75 // cached local tty parameters
80 smallint curoff
; // unknown:0 cursor on:-1 cursor off:1
81 char attrbuf
[sizeof("0;1;5;30;40m")];
83 struct screen_info remote
;
84 // saved local tty terminfo
85 struct termios term_orig
;
86 char vcsa_name
[sizeof(DEV_VCSA
"NN")];
89 #define G (*ptr_to_globals)
90 #define INIT_G() do { \
91 SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
92 G.width = G.height = UINT_MAX; \
98 FLAG_c
, // create device if need
102 FLAG_d
, // dump screen
103 FLAG_f
, // follow cursor
104 FLAG_F
, // framebuffer
106 #define FLAG(x) (1 << FLAG_##x)
107 #define BW (option_mask32 & FLAG(n))
109 static void putcsi(const char *s
)
111 fputs(ESC
"[", stdout
);
115 static void clrscr(void)
117 // Home, clear till end of screen
118 putcsi("1;1H" ESC
"[J");
122 static void set_cursor(int state
)
124 if (G
.curoff
!= state
) {
127 bb_putchar("h?l"[1 + state
]);
131 static void gotoxy(int col
, int line
)
133 if (G
.col
!= col
|| G
.line
!= line
) {
136 printf(ESC
"[%u;%uH", line
+ 1, col
+ 1);
140 static void cleanup(int code
) NORETURN
;
141 static void cleanup(int code
)
143 set_cursor(CURSOR_ON
);
144 tcsetattr(G
.kbd_fd
, TCSANOW
, &G
.term_orig
);
145 if (ENABLE_FEATURE_CLEAN_UP
) {
152 if (code
> EXIT_FAILURE
)
153 kill_myself_with_sig(code
);
157 static void screen_read_close(void)
163 // Close & re-open vcsa in case they have swapped virtual consoles
164 vcsa_fd
= xopen(G
.vcsa_name
, O_RDONLY
);
165 xread(vcsa_fd
, &G
.remote
, 4);
166 i
= G
.remote
.cols
* 2;
167 G
.first_line_offset
= G
.y
* i
;
169 if (G
.data
== NULL
) {
171 G
.data
= xzalloc(2 * i
);
174 cleanup(EXIT_FAILURE
);
176 data
= G
.data
+ G
.current
;
177 xread(vcsa_fd
, data
, G
.size
);
179 for (i
= 0; i
< G
.remote
.lines
; i
++) {
180 for (j
= 0; j
< G
.remote
.cols
; j
++, NEXT(data
)) {
181 unsigned x
= j
- G
.x
; // if will catch j < G.x too
182 unsigned y
= i
- G
.y
; // if will catch i < G.y too
184 if (y
>= G
.height
|| x
>= G
.width
)
187 uint8_t ch
= CHAR(data
);
189 CHAR(data
) = ch
| 0x40;
197 static void screen_char(char *data
)
201 uint8_t attr
= ATTR(data
);
203 if (option_mask32
& FLAG(F
)) {
206 attr_diff
= G
.last_attr
^ attr
;
208 // Attribute layout for VGA compatible text videobuffer:
214 // 00000000 <- lsb bit on the right
215 // bold text / text 8th bit
219 // TODO: apparently framebuffer-based console uses different layout
220 // (bug? attempt to get 8th text bit in better position?)
225 // 00000000 <- lsb bit on the right
231 // converting RGB color bit triad to BGR:
232 static const char color
[8] = "04261537";
233 const uint8_t fg_mask
= 0x07, bold_mask
= 0x08;
234 const uint8_t bg_mask
= 0x70, blink_mask
= 0x80;
239 // (G.last_attr & ~attr) has 1 only where
240 // G.last_attr has 1 but attr has 0.
241 // Here we check whether we have transition
242 // bold->non-bold or blink->non-blink:
243 if (G
.last_attr
< 0 // initial value
244 || ((G
.last_attr
& ~attr
) & (bold_mask
| blink_mask
)) != 0
246 *ptr
++ = '0'; // "reset all attrs"
248 // must set fg & bg, maybe need to set bold or blink:
249 attr_diff
= attr
| ~(bold_mask
| blink_mask
);
252 if (attr_diff
& bold_mask
) {
256 if (attr_diff
& blink_mask
) {
260 if (attr_diff
& fg_mask
) {
262 *ptr
++ = color
[attr
& fg_mask
];
265 if (attr_diff
& bg_mask
) {
267 *ptr
++ = color
[(attr
& bg_mask
) >> 4];
268 ptr
++; // last attribute
270 if (ptr
!= G
.attrbuf
) {
281 static void screen_dump(void)
285 int linecnt
= G
.remote
.lines
- G
.y
;
286 char *data
= G
.data
+ G
.current
+ G
.first_line_offset
;
289 for (line
= 0; line
< linecnt
&& line
< G
.height
; line
++) {
291 for (col
= 0; col
< G
.remote
.cols
; col
++, NEXT(data
)) {
292 unsigned tty_col
= col
- G
.x
; // if will catch col < G.x too
294 if (tty_col
>= G
.width
)
297 if (BW
&& CHAR(data
) == ' ')
299 while (linefeed_cnt
!= 0) {
300 //bb_putchar('\r'); - tty driver does it for us
312 static void curmove(void)
314 unsigned cx
= G
.remote
.cursor_x
- G
.x
;
315 unsigned cy
= G
.remote
.cursor_y
- G
.y
;
316 int cursor
= CURSOR_OFF
;
318 if (cx
< G
.width
&& cy
< G
.height
) {
325 static void create_cdev_if_doesnt_exist(const char* name
, dev_t dev
)
327 int fd
= open(name
, O_RDONLY
);
330 else if (errno
== ENOENT
)
331 mknod(name
, S_IFCHR
| 0660, dev
);
334 static NOINLINE
void start_shell_in_child(const char* tty_name
)
338 struct termios termchild
;
339 const char *shell
= get_shell_name();
341 signal(SIGHUP
, SIG_IGN
);
342 // set tty as a controlling tty
344 // make tty to be input, output, error
346 xopen(tty_name
, O_RDWR
); // uses fd 0
349 ioctl(0, TIOCSCTTY
, 1);
350 tcsetpgrp(0, getpid());
351 tcgetattr(0, &termchild
);
352 termchild
.c_lflag
|= ECHO
;
353 termchild
.c_oflag
|= ONLCR
| XTABS
;
354 termchild
.c_iflag
|= ICRNL
;
355 termchild
.c_iflag
&= ~IXOFF
;
356 tcsetattr_stdin_TCSANOW(&termchild
);
357 execl(shell
, shell
, "-i", (char *) NULL
);
358 bb_simple_perror_msg_and_die(shell
);
362 int conspy_main(int argc
, char **argv
) MAIN_EXTERNALLY_VISIBLE
;
363 int conspy_main(int argc UNUSED_PARAM
, char **argv
)
365 char tty_name
[sizeof(DEV_TTY
"NN")];
366 #define keybuf bb_common_bufsiz1
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
;
386 strcpy(G
.vcsa_name
, DEV_VCSA
);
388 opt_complementary
= "x+:y+"; // numeric params
389 opts
= getopt32(argv
, "vcQsndfFx:y:", &G
.x
, &G
.y
);
393 ttynum
= xatou_range(argv
[0], 0, 63);
394 sprintf(G
.vcsa_name
+ sizeof(DEV_VCSA
)-1, "%u", ttynum
);
396 sprintf(tty_name
, "%s%u", DEV_TTY
, ttynum
);
397 if (opts
& FLAG(c
)) {
398 if ((opts
& (FLAG(s
)|FLAG(v
))) != FLAG(v
))
399 create_cdev_if_doesnt_exist(tty_name
, makedev(4, ttynum
));
400 create_cdev_if_doesnt_exist(G
.vcsa_name
, makedev(7, 128 + ttynum
));
402 if ((opts
& FLAG(s
)) && ttynum
) {
403 start_shell_in_child(tty_name
);
407 if (opts
& FLAG(d
)) {
413 bb_signals(BB_FATAL_SIGS
, cleanup
);
415 // All characters must be passed through to us unaltered
416 G
.kbd_fd
= xopen(CURRENT_TTY
, O_RDONLY
);
417 tcgetattr(G
.kbd_fd
, &G
.term_orig
);
418 termbuf
= G
.term_orig
;
419 termbuf
.c_iflag
&= ~(BRKINT
|INLCR
|ICRNL
|IXON
|IXOFF
|IUCLC
|IXANY
|IMAXBEL
);
420 //termbuf.c_oflag &= ~(OPOST); - no, we still want \n -> \r\n
421 termbuf
.c_lflag
&= ~(ISIG
|ICANON
|ECHO
);
422 termbuf
.c_cc
[VMIN
] = 1;
423 termbuf
.c_cc
[VTIME
] = 0;
424 tcsetattr(G
.kbd_fd
, TCSANOW
, &termbuf
);
426 poll_timeout_ms
= 250;
433 // in the first loop G.width = G.height = 0: refresh
436 get_terminal_width_height(G
.kbd_fd
, &G
.width
, &G
.height
);
437 if (option_mask32
& FLAG(f
)) {
438 int nx
= G
.remote
.cursor_x
- G
.width
+ 1;
439 int ny
= G
.remote
.cursor_y
- G
.height
+ 1;
441 if (G
.remote
.cursor_x
< G
.x
) {
442 G
.x
= G
.remote
.cursor_x
;
443 i
= 0; // force refresh
447 i
= 0; // force refresh
449 if (G
.remote
.cursor_y
< G
.y
) {
450 G
.y
= G
.remote
.cursor_y
;
451 i
= 0; // force refresh
455 i
= 0; // force refresh
459 // Scan console data and redraw our tty where needed
460 old
= G
.data
+ G
.current
;
461 G
.current
= G
.size
- G
.current
;
462 data
= G
.data
+ G
.current
;
464 if (i
!= G
.width
|| j
!= G
.height
) {
468 // For each remote line
469 old
+= G
.first_line_offset
;
470 data
+= G
.first_line_offset
;
471 for (i
= G
.y
; i
< G
.remote
.lines
; i
++) {
472 char *first
= NULL
; // first char which needs updating
473 char *last
= last
; // last char which needs updating
474 unsigned iy
= i
- G
.y
;
478 for (j
= 0; j
< G
.remote
.cols
; j
++, NEXT(old
), NEXT(data
)) {
479 unsigned jx
= j
- G
.x
; // if will catch j >= G.x too
481 if (jx
< G
.width
&& DATA(data
) != DATA(old
)) {
490 // Rewrite updated data on the local screen
491 for (; first
<= last
; NEXT(first
))
498 // Wait for local user keypresses
503 switch (poll(&pfd
, 1, poll_timeout_ms
)) {
511 G
.nokeys
= G
.escape_count
= 0;
514 // Read the keys pressed
515 k
= keybuf
+ G
.key_count
;
516 bytes_read
= read(G
.kbd_fd
, k
, sizeof(keybuf
) - G
.key_count
);
520 // Do exit processing
521 if (!(option_mask32
& FLAG(Q
))) {
522 for (i
= 0; i
< bytes_read
; i
++) {
525 if (++G
.escape_count
>= 3)
526 cleanup(EXIT_SUCCESS
);
530 poll_timeout_ms
= 250;
531 if (option_mask32
& FLAG(v
)) continue;
533 // Insert all keys pressed into the virtual console's input
534 // buffer. Don't do this if the virtual console is in scan
535 // code mode - giving ASCII characters to a program expecting
536 // scan codes will confuse it.
537 G
.key_count
+= bytes_read
;
538 if (G
.escape_count
== 0) {
542 handle
= xopen(tty_name
, O_WRONLY
);
543 result
= ioctl(handle
, KDGKBMODE
, &kbd_mode
);
548 if (kbd_mode
!= K_XLATE
&& kbd_mode
!= K_UNICODE
) {
549 G
.key_count
= 0; // scan code mode
551 for (; G
.key_count
!= 0; p
++, G
.key_count
--) {
552 result
= ioctl(handle
, TIOCSTI
, p
);
554 memmove(keybuf
, p
, G
.key_count
);
557 // If there is an application on console which reacts
558 // to keypresses, we need to make our first sleep
559 // shorter to quickly redraw whatever it printed there.
560 poll_timeout_ms
= 20;
563 // We sometimes get spurious IO errors on the TTY
564 // as programs close and re-open it
565 else if (errno
!= EIO
|| ++G
.ioerror_count
> 4) {
566 if (ENABLE_FEATURE_CLEAN_UP
)
570 // Close & re-open tty in case they have
571 // swapped virtual consoles
576 cleanup(EXIT_FAILURE
);