123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- /* vi: set sw=4 ts=4: */
- /*
- * A text-mode VNC like program for Linux virtual terminals.
- *
- * pascal.bellard@ads-lu.com
- *
- * Based on Russell Stuart's conspy.c
- * http://ace-host.stuart.id.au/russell/files/conspy.c
- *
- * Licensed under GPLv2 or later, see file LICENSE in this source tree.
- */
- //config:config CONSPY
- //config: bool "conspy (10 kb)"
- //config: default y
- //config: help
- //config: A text-mode VNC like program for Linux virtual terminals.
- //config: example: conspy NUM shared access to console num
- //config: or conspy -nd NUM screenshot of console num
- //config: or conspy -cs NUM poor man's GNU screen like
- //applet:IF_CONSPY(APPLET(conspy, BB_DIR_BIN, BB_SUID_DROP))
- //kbuild:lib-$(CONFIG_CONSPY) += conspy.o
- //usage:#define conspy_trivial_usage
- //usage: "[-vcsndfFQ] [-x COL] [-y LINE] [CONSOLE_NO]"
- //usage:#define conspy_full_usage "\n\n"
- //usage: "A text-mode VNC like program for Linux virtual consoles."
- //usage: "\nTo exit, quickly press ESC 3 times."
- //usage: "\n"
- //usage: "\n -v Don't send keystrokes to the console"
- //usage: "\n -c Create missing /dev/{tty,vcsa}N"
- //usage: "\n -s Open a SHELL session"
- //usage: "\n -n Black & white"
- //usage: "\n -d Dump console to stdout"
- //usage: "\n -f Follow cursor"
- //usage: "\n -F Assume console is on a framebuffer device"
- //usage: "\n -Q Disable exit on ESC-ESC-ESC"
- //usage: "\n -x COL Starting column"
- //usage: "\n -y LINE Starting line"
- #include "libbb.h"
- #include "common_bufsiz.h"
- #include <sys/kd.h>
- #define ESC "\033"
- #define CURSOR_ON -1
- #define CURSOR_OFF 1
- #define DEV_TTY "/dev/tty"
- #define DEV_VCSA "/dev/vcsa"
- struct screen_info {
- unsigned char lines, cols, cursor_x, cursor_y;
- };
- #define CHAR(x) (*(uint8_t*)(x))
- #define ATTR(x) (((uint8_t*)(x))[1])
- #define NEXT(x) ((x) += 2)
- #define DATA(x) (*(uint16_t*)(x))
- struct globals {
- char* data;
- int size;
- int x, y;
- int kbd_fd;
- int ioerror_count;
- int key_count;
- int escape_count;
- int nokeys;
- int current;
- int first_line_offset;
- int last_attr;
- // cached local tty parameters
- unsigned width;
- unsigned height;
- unsigned col;
- unsigned line;
- smallint curoff; // unknown:0 cursor on:-1 cursor off:1
- char attrbuf[sizeof("0;1;5;30;40m")];
- // remote console
- struct screen_info remote;
- // saved local tty terminfo
- struct termios term_orig;
- char vcsa_name[sizeof(DEV_VCSA "NN")];
- };
- #define G (*ptr_to_globals)
- #define INIT_G() do { \
- SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
- G.width = G.height = UINT_MAX; \
- G.last_attr--; \
- } while (0)
- enum {
- FLAG_v, // view only
- FLAG_c, // create device if need
- FLAG_Q, // never exit
- FLAG_s, // session
- FLAG_n, // no colors
- FLAG_d, // dump screen
- FLAG_f, // follow cursor
- FLAG_F, // framebuffer
- };
- #define FLAG(x) (1 << FLAG_##x)
- #define BW (option_mask32 & FLAG(n))
- static void putcsi(const char *s)
- {
- fputs_stdout(ESC"[");
- fputs_stdout(s);
- }
- static void clrscr(void)
- {
- // Home, clear till end of screen
- putcsi("1;1H" ESC"[J");
- G.col = G.line = 0;
- }
- static void set_cursor(int state)
- {
- if (G.curoff != state) {
- G.curoff = state;
- putcsi("?25");
- bb_putchar("h?l"[1 + state]);
- }
- }
- static void gotoxy(int col, int line)
- {
- if (G.col != col || G.line != line) {
- G.col = col;
- G.line = line;
- printf(ESC"[%u;%uH", line + 1, col + 1);
- }
- }
- static void cleanup(int code) NORETURN;
- static void cleanup(int code)
- {
- set_cursor(CURSOR_ON);
- tcsetattr(G.kbd_fd, TCSANOW, &G.term_orig);
- if (ENABLE_FEATURE_CLEAN_UP) {
- close(G.kbd_fd);
- }
- // Reset attributes
- if (!BW)
- putcsi("0m");
- bb_putchar('\n');
- if (code > EXIT_FAILURE)
- kill_myself_with_sig(code);
- exit(code);
- }
- static void screen_read_close(void)
- {
- unsigned i, j;
- int vcsa_fd;
- char *data;
- // Close & re-open vcsa in case they have swapped virtual consoles
- vcsa_fd = xopen(G.vcsa_name, O_RDONLY);
- xread(vcsa_fd, &G.remote, 4);
- i = G.remote.cols * 2;
- G.first_line_offset = G.y * i;
- i *= G.remote.lines;
- if (G.data == NULL) {
- G.size = i;
- G.data = xzalloc(2 * i);
- }
- if (G.size != i) {
- cleanup(EXIT_FAILURE);
- }
- data = G.data + G.current;
- xread(vcsa_fd, data, G.size);
- close(vcsa_fd);
- for (i = 0; i < G.remote.lines; i++) {
- for (j = 0; j < G.remote.cols; j++, NEXT(data)) {
- unsigned x = j - G.x; // if will catch j < G.x too
- unsigned y = i - G.y; // if will catch i < G.y too
- if (y >= G.height || x >= G.width)
- DATA(data) = 0;
- else {
- uint8_t ch = CHAR(data);
- if (ch < ' ')
- CHAR(data) = ch | 0x40;
- else if (ch > 0x7e)
- CHAR(data) = '?';
- }
- }
- }
- }
- static void screen_char(char *data)
- {
- if (!BW) {
- uint8_t attr_diff;
- uint8_t attr = ATTR(data);
- if (option_mask32 & FLAG(F)) {
- attr >>= 1;
- }
- attr_diff = G.last_attr ^ attr;
- if (attr_diff) {
- // Attribute layout for VGA compatible text videobuffer:
- // blinking text
- // |red bkgd
- // ||green bkgd
- // |||blue bkgd
- // vvvv
- // 00000000 <- lsb bit on the right
- // bold text / text 8th bit
- // red text
- // green text
- // blue text
- // TODO: apparently framebuffer-based console uses different layout
- // (bug? attempt to get 8th text bit in better position?)
- // red bkgd
- // |green bkgd
- // ||blue bkgd
- // vvv
- // 00000000 <- lsb bit on the right
- // bold text
- // red text
- // green text
- // blue text
- // text 8th bit
- // converting RGB color bit triad to BGR:
- static const char color[8] = "04261537";
- const uint8_t fg_mask = 0x07, bold_mask = 0x08;
- const uint8_t bg_mask = 0x70, blink_mask = 0x80;
- char *ptr;
- ptr = G.attrbuf;
- // (G.last_attr & ~attr) has 1 only where
- // G.last_attr has 1 but attr has 0.
- // Here we check whether we have transition
- // bold->non-bold or blink->non-blink:
- if (G.last_attr < 0 // initial value
- || ((G.last_attr & ~attr) & (bold_mask | blink_mask)) != 0
- ) {
- *ptr++ = '0'; // "reset all attrs"
- *ptr++ = ';';
- // must set fg & bg, maybe need to set bold or blink:
- attr_diff = attr | ~(bold_mask | blink_mask);
- }
- G.last_attr = attr;
- if (attr_diff & bold_mask) {
- *ptr++ = '1';
- *ptr++ = ';';
- }
- if (attr_diff & blink_mask) {
- *ptr++ = '5';
- *ptr++ = ';';
- }
- if (attr_diff & fg_mask) {
- *ptr++ = '3';
- *ptr++ = color[attr & fg_mask];
- *ptr++ = ';';
- }
- if (attr_diff & bg_mask) {
- *ptr++ = '4';
- *ptr++ = color[(attr & bg_mask) >> 4];
- ptr++; // last attribute
- }
- if (ptr != G.attrbuf) {
- ptr[-1] = 'm';
- *ptr = '\0';
- putcsi(G.attrbuf);
- }
- }
- }
- putchar(CHAR(data));
- G.col++;
- }
- static void screen_dump(void)
- {
- int linefeed_cnt;
- int line, col;
- int linecnt = G.remote.lines - G.y;
- char *data = G.data + G.current + G.first_line_offset;
- linefeed_cnt = 0;
- for (line = 0; line < linecnt && line < G.height; line++) {
- int space_cnt = 0;
- for (col = 0; col < G.remote.cols; col++, NEXT(data)) {
- unsigned tty_col = col - G.x; // if will catch col < G.x too
- if (tty_col >= G.width)
- continue;
- space_cnt++;
- if (BW && CHAR(data) == ' ')
- continue;
- while (linefeed_cnt != 0) {
- //bb_putchar('\r'); - tty driver does it for us
- bb_putchar('\n');
- linefeed_cnt--;
- }
- while (--space_cnt)
- bb_putchar(' ');
- screen_char(data);
- }
- linefeed_cnt++;
- }
- }
- static void curmove(void)
- {
- unsigned cx = G.remote.cursor_x - G.x;
- unsigned cy = G.remote.cursor_y - G.y;
- int cursor = CURSOR_OFF;
- if (cx < G.width && cy < G.height) {
- gotoxy(cx, cy);
- cursor = CURSOR_ON;
- }
- set_cursor(cursor);
- }
- static void create_cdev_if_doesnt_exist(const char* name, dev_t dev)
- {
- int fd = open(name, O_RDONLY);
- if (fd != -1)
- close(fd);
- else if (errno == ENOENT)
- mknod(name, S_IFCHR | 0660, dev);
- }
- static NOINLINE void start_shell_in_child(const char* tty_name)
- {
- int pid = xvfork();
- if (pid == 0) {
- struct termios termchild;
- const char *shell = get_shell_name();
- signal(SIGHUP, SIG_IGN);
- // set tty as a controlling tty
- setsid();
- // make tty to be input, output, error
- close(0);
- xopen(tty_name, O_RDWR); // uses fd 0
- xdup2(0, 1);
- xdup2(0, 2);
- ioctl(0, TIOCSCTTY, 1);
- tcsetpgrp(0, getpid());
- tcgetattr(0, &termchild);
- termchild.c_lflag |= ECHO;
- termchild.c_oflag |= ONLCR | XTABS;
- termchild.c_iflag |= ICRNL;
- termchild.c_iflag &= ~IXOFF;
- tcsetattr_stdin_TCSANOW(&termchild);
- execl(shell, shell, "-i", (char *) NULL);
- bb_simple_perror_msg_and_die(shell);
- }
- }
- int conspy_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
- int conspy_main(int argc UNUSED_PARAM, char **argv)
- {
- char tty_name[sizeof(DEV_TTY "NN")];
- unsigned opts;
- unsigned ttynum;
- int poll_timeout_ms;
- #if ENABLE_LONG_OPTS
- static const char conspy_longopts[] ALIGN1 =
- "viewonly\0" No_argument "v"
- "createdevice\0" No_argument "c"
- "neverquit\0" No_argument "Q"
- "session\0" No_argument "s"
- "nocolors\0" No_argument "n"
- "dump\0" No_argument "d"
- "follow\0" No_argument "f"
- "framebuffer\0" No_argument "F"
- ;
- #endif
- #define keybuf bb_common_bufsiz1
- setup_common_bufsiz();
- INIT_G();
- strcpy(G.vcsa_name, DEV_VCSA);
- // numeric params
- opts = getopt32long(argv, "vcQsndfFx:+y:+", conspy_longopts, &G.x, &G.y);
- argv += optind;
- ttynum = 0;
- if (argv[0]) {
- ttynum = xatou_range(argv[0], 0, 63);
- sprintf(G.vcsa_name + sizeof(DEV_VCSA)-1, "%u", ttynum);
- }
- sprintf(tty_name, "%s%u", DEV_TTY, ttynum);
- if (opts & FLAG(c)) {
- if ((opts & (FLAG(s)|FLAG(v))) != FLAG(v))
- create_cdev_if_doesnt_exist(tty_name, makedev(4, ttynum));
- create_cdev_if_doesnt_exist(G.vcsa_name, makedev(7, 128 + ttynum));
- }
- if ((opts & FLAG(s)) && ttynum) {
- start_shell_in_child(tty_name);
- }
- screen_read_close();
- if (opts & FLAG(d)) {
- screen_dump();
- bb_putchar('\n');
- return 0;
- }
- bb_signals(BB_FATAL_SIGS, cleanup);
- G.kbd_fd = xopen(CURRENT_TTY, O_RDONLY);
- // All characters must be passed through to us unaltered
- set_termios_to_raw(G.kbd_fd, &G.term_orig, 0
- | TERMIOS_CLEAR_ISIG // no signals on ^C ^Z etc
- | TERMIOS_RAW_INPUT // turn off all input conversions
- );
- //Note: termios.c_oflag &= ~(OPOST); - no, we still want \n -> \r\n
- poll_timeout_ms = 250;
- while (1) {
- struct pollfd pfd;
- int bytes_read;
- int i, j;
- char *data, *old;
- // in the first loop G.width = G.height = 0: refresh
- i = G.width;
- j = G.height;
- get_terminal_width_height(G.kbd_fd, &G.width, &G.height);
- if (option_mask32 & FLAG(f)) {
- int nx = G.remote.cursor_x - G.width + 1;
- int ny = G.remote.cursor_y - G.height + 1;
- if (G.remote.cursor_x < G.x) {
- G.x = G.remote.cursor_x;
- i = 0; // force refresh
- }
- if (nx > G.x) {
- G.x = nx;
- i = 0; // force refresh
- }
- if (G.remote.cursor_y < G.y) {
- G.y = G.remote.cursor_y;
- i = 0; // force refresh
- }
- if (ny > G.y) {
- G.y = ny;
- i = 0; // force refresh
- }
- }
- // Scan console data and redraw our tty where needed
- old = G.data + G.current;
- G.current = G.size - G.current;
- data = G.data + G.current;
- screen_read_close();
- if (i != G.width || j != G.height) {
- clrscr();
- screen_dump();
- } else {
- // For each remote line
- old += G.first_line_offset;
- data += G.first_line_offset;
- for (i = G.y; i < G.remote.lines; i++) {
- char *first = NULL; // first char which needs updating
- char *last = last; // last char which needs updating
- unsigned iy = i - G.y;
- if (iy >= G.height)
- break;
- for (j = 0; j < G.remote.cols; j++, NEXT(old), NEXT(data)) {
- unsigned jx = j - G.x; // if will catch j >= G.x too
- if (jx < G.width && DATA(data) != DATA(old)) {
- last = data;
- if (!first) {
- first = data;
- gotoxy(jx, iy);
- }
- }
- }
- if (first) {
- // Rewrite updated data on the local screen
- for (; first <= last; NEXT(first))
- screen_char(first);
- }
- }
- }
- curmove();
- // Wait for local user keypresses
- fflush_all();
- pfd.fd = G.kbd_fd;
- pfd.events = POLLIN;
- bytes_read = 0;
- switch (poll(&pfd, 1, poll_timeout_ms)) {
- char *k;
- case -1:
- if (errno != EINTR)
- goto abort;
- break;
- case 0:
- if (++G.nokeys >= 4)
- G.nokeys = G.escape_count = 0;
- break;
- default:
- // Read the keys pressed
- k = keybuf + G.key_count;
- bytes_read = read(G.kbd_fd, k, COMMON_BUFSIZE - G.key_count);
- if (bytes_read < 0)
- goto abort;
- // Do exit processing
- if (!(option_mask32 & FLAG(Q))) {
- for (i = 0; i < bytes_read; i++) {
- if (k[i] != '\033')
- G.escape_count = -1;
- if (++G.escape_count >= 3)
- cleanup(EXIT_SUCCESS);
- }
- }
- }
- poll_timeout_ms = 250;
- if (option_mask32 & FLAG(v)) continue;
- // Insert all keys pressed into the virtual console's input
- // buffer. Don't do this if the virtual console is in scan
- // code mode - giving ASCII characters to a program expecting
- // scan codes will confuse it.
- G.key_count += bytes_read;
- if (G.escape_count == 0) {
- int handle, result;
- long kbd_mode;
- handle = xopen(tty_name, O_WRONLY);
- result = ioctl(handle, KDGKBMODE, &kbd_mode);
- if (result >= 0) {
- char *p = keybuf;
- G.ioerror_count = 0;
- if (kbd_mode != K_XLATE && kbd_mode != K_UNICODE) {
- G.key_count = 0; // scan code mode
- }
- for (; G.key_count != 0; p++, G.key_count--) {
- result = ioctl(handle, TIOCSTI, p);
- if (result < 0) {
- memmove(keybuf, p, G.key_count);
- break;
- }
- // If there is an application on console which reacts
- // to keypresses, we need to make our first sleep
- // shorter to quickly redraw whatever it printed there.
- poll_timeout_ms = 20;
- }
- }
- // We sometimes get spurious IO errors on the TTY
- // as programs close and re-open it
- else if (errno != EIO || ++G.ioerror_count > 4) {
- if (ENABLE_FEATURE_CLEAN_UP)
- close(handle);
- goto abort;
- }
- // Close & re-open tty in case they have
- // swapped virtual consoles
- close(handle);
- }
- } /* while (1) */
- abort:
- cleanup(EXIT_FAILURE);
- }
|