/*
* conspy
* ------
*
* A text-mode VNC like program for Linux virtual terminals.
*
* Author: Russell Stuart, russell-conspy@stuart.id.au
* 22/05/2003
*
* To compile:
* gcc -Werror -Wall -Wextra -O2 --std=c99 -lncurses -o conspy conspy.c
*
*
* License
* -------
*
* Copyright (c) 2009-2014,2015,2016 Russell Stuart.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* The copyright holders grant you an additional permission under Section 7
* of the GNU Affero General Public License, version 3, exempting you from
* the requirement in Section 6 of the GNU General Public License, version 3,
* to accompany Corresponding Source with Installation Information for the
* Program or any work based on the Program. You are still required to
* comply with all other Section 6 requirements to provide Corresponding
* Source.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* GNU/kFreeBSD has different #define's */
#if defined(__FreeBSD_kernel__)
#define K_UNICODE K_CODE
#define IUCLC 0
#endif
extern int errno;
/*
* Version info.
*/
static char conspy_date[] = "2016-02-16";
static char conspy_version[] = "1.14";
/*
* VGA colour definitions, as found in a nibble in an attribute
* byte within VGA video memory.
*/
#define VGA_BLACK 0x00
#define VGA_BLUE 0x01
#define VGA_GREEN 0x02
#define VGA_CYAN 0x03
#define VGA_RED 0x04
#define VGA_MAGENTA 0x05
#define VGA_YELLOW 0x06
#define VGA_WHITE 0x07
/*
* Box characters used by the Linux console.
*/
#define IBM_BLOCK 0x0c
#define IBM_BOARD 0x09
#define IBM_BTEE 0xcb
#define IBM_CKBOARD 0x0a
#define IBM_DARROW 0x19
#define IBM_DEGREE 0xb0
#define IBM_GEQUAL 0x14
#define IBM_HLINE 0xca
#define IBM_LANTERN 0xdf
#define IBM_LARROW 0x16
#define IBM_LLCORNER 0xc3
#define IBM_LRCORNER 0xc9
#define IBM_LTEE 0xc7
#define IBM_PI 0x1f
#define IBM_PLMINUS 0xb1
#define IBM_RTEE 0xcd
#define IBM_STERLING 0xa3
#define IBM_TTEE 0xce
#define IBM_UARROW 0x18
#define IBM_ULCORNER 0xc6
#define IBM_URCORNER 0xcc
#define IBM_VLINE 0xc5
/*
* This is the original IBM PC charcter set. I thought this is what the
* Linux console would use, but apparently not.
*/
#if 0
#define IBM_BLOCK 0xdb
#define IBM_BTEE 0xc1
#define IBM_BULLET 0xf9
#define IBM_DARROW 0x19
#define IBM_D_BTEE 0xca
#define IBM_DEGREE 0xf8
#define IBM_D_HLINE 0xcd
#define IBM_DIAMOND 0x04
#define IBM_D_LLCORNER 0xc8
#define IBM_D_LRCORNER 0xbc
#define IBM_D_LTEE 0xcc
#define IBM_D_RTEE 0xb9
#define IBM_DS_BTEE 0xcf
#define IBM_DS_LLCORNER 0xd3
#define IBM_DS_LRCORNER 0xbd
#define IBM_DS_LTEE 0xc7
#define IBM_DS_RTEE 0xb6
#define IBM_DS_TTEE 0xd1
#define IBM_DS_ULCORNER 0xd6
#define IBM_DS_URCORNER 0xb7
#define IBM_DS_XCROSS 0xd7
#define IBM_D_TTEE 0xcb
#define IBM_D_ULCORNER 0xc9
#define IBM_D_URCORNER 0xbb
#define IBM_D_VLINE 0xba
#define IBM_D_XCROSS 0xce
#define IBM_GEQUAL 0xf2
#define IBM_HLINE 0xc4
#define IBM_LANTERN 0x0f
#define IBM_LARROW 0x1b
#define IBM_LLCORNER 0xc0
#define IBM_LRCORNER 0xd9
#define IBM_LTEE 0xc3
#define IBM_PLMINUS 0xf1
#define IBM_RARROW 0x1a
#define IBM_RTEE 0xb4
#define IBM_SD_BTEE 0xd0
#define IBM_SD_LLCORNER 0xd4
#define IBM_SD_LRCORNER 0xbe
#define IBM_SD_LTEE 0xc6
#define IBM_SD_RTEE 0xb5
#define IBM_SD_TTEE 0xd2
#define IBM_SD_ULCORNER 0xd5
#define IBM_SD_URCORNER 0xb8
#define IBM_SD_XCROSS 0xd8
#define IBM_STERLING 0x9c
#define IBM_TTEE 0xc2
#define IBM_UARROW 0x18
#define IBM_ULCORNER 0xda
#define IBM_URCORNER 0xbf
#define IBM_VLINE 0xb3
#define IBM_XCROSS 0xc5
#endif
/*
* This function maps a VGA colour pair to a curses COLOR_PAIR()
* number. In the curses scheme colour pair 0 must be white text
* on a black background, so the origin is moved to there.
*/
#define VGA_PAIR(foreground, background) \
(((foreground) + ((background) << 3)) ^ 0x07)
/*
* Forward declarations.
*/
static void cleanup();
static void conspy(int use_colour);
static void finish(int signal);
static void init_cursesbox();
static void process_command_line(int argc, char** argv);
static int setup();
static void syserror(char* message, ...);
static void usage(char* message, ...);
/*
* Local variables.
*/
static char* me;
static struct termios old_termios;
static int opt_columns;
static int opt_lines;
static int opt_viewonly;
static int tty_handle = -1;
static char tty_name[20];
static int device_handle = -1;
static char device_name[20];
/*
* This array translates a VGA colour (defined above) to a
* CURSES colour.
*/
static short colour_map[] =
{ COLOR_BLACK, /* VGA_BLACK */
COLOR_BLUE, /* VGA_BLUE */
COLOR_GREEN, /* VGA_GREEN */
COLOR_CYAN, /* VGA_CYAN */
COLOR_RED, /* VGA_RED */
COLOR_MAGENTA, /* VGA_MAGENTA */
COLOR_YELLOW, /* VGA_YELLOW */
COLOR_WHITE, /* VGA_WHITE */
};
/*
* Special IBM characters & their translations.
*/
static unsigned short cursesbox[256];
/*
* A character as it appears in the VGA video buffer.
*/
struct vidchar {
#if 0
unsigned char vidchar_char; /* The IBM-ASCII Char code */
unsigned char vidchar_attribute; /* Colour/blink/bold spec */
#define VIDCHAR_CHAR(vidchar) ((vidchar)->vidchar_char)
#define VIDCHAR_ATTRIBUTE(vidchar) ((vidchar)->vidchar_attribute)
#else
unsigned short vidchar_charattr; /* Attr in msb, char in lsb */
#define VIDCHAR_CHAR(vidchar) ((vidchar)->vidchar_charattr & 0xFF)
#define VIDCHAR_ATTRIBUTE(vidchar) ((vidchar)->vidchar_charattr >> 8)
#endif
};
/*
* The data returned by reading a /dev/vcsa device.
*/
struct vidbuf {
unsigned char vidbuf_lines; /* Line on screen */
unsigned char vidbuf_columns; /* Columns on screen */
unsigned char vidbuf_curcolumn; /* Column cursor is in */
unsigned char vidbuf_curline; /* Line cursor is in */
struct vidchar vidbuf_chars[0]; /* Char in VGA video buf */
};
#define VIDBUF_SIZE(cols, lines) (sizeof(struct vidbuf) + cols * lines * sizeof(struct vidchar))
/*
* Options we allow.
*/
static struct option options[] =
{
{"geometry", 1, 0, 'g'},
{"version", 0, 0, 'V'},
{"viewonly", 0, 0, 'v'},
{0,0,0,0},
};
/*
* Entry point.
*/
int main(int argc, char** argv)
{
int use_colour;
me = strrchr(argv[0], '/');
me = me == 0 ? argv[0] : me + 1;
process_command_line(argc, argv);
use_colour = setup();
init_cursesbox();
conspy(use_colour);
cleanup();
/*
* Clear screen & home cursor. Added when _I_ became confused
* as to whether this program was running or not(!)
*/
if (tigetstr("clear") != (char*)0)
{
putp(tigetstr("clear"));
fflush(stdout);
}
return 0;
}
/*
* Print our a usage message and exit.
*/
static void usage(char* message, ...)
{
va_list list;
if (message != 0)
{
fprintf(stderr, "%s: ", me);
va_start(list, message);
vfprintf(stderr, message, list);
va_end(list);
fprintf(stderr, ".\n");
}
fprintf(stderr, "usage: %s [options] [virtual_console].\n", me);
fprintf(stderr, "options:\n");
fprintf(stderr, " -g G,--geometry=G Console size is G, format COLSxROWS.\n");
fprintf(stderr, " -V,--version Print %s's version number and exit.\n", me);
fprintf(stderr, " -v,--viewonly Don't send keystrokes to the console.\n");
fprintf(stderr, "virtual_console:\n");
fprintf(stderr, " omitted Track the current console.\n");
fprintf(stderr, " 1..63 Virtual console N.\n");
fprintf(stderr, "To exit, quickly press escape 3 times.\n");
exit(1);
}
/*
* Process the command line.
*/
static void process_command_line(int argc, char** argv)
{
char* end;
int opt;
size_t optindex;
char opts[(sizeof(options) / sizeof(*options))*2 + 1];
char vcc_name[sizeof(device_name)];
int vcc_errno;
char vcsa_name[sizeof(device_name)];
int vcsa_errno;
char* virtual_console;
opt = 0;
for (optindex = 0; optindex < sizeof(options)/sizeof(*options); optindex += 1)
{
opts[opt++] = options[optindex].val;
if (options[optindex].has_arg)
opts[opt++] = ':';
}
opts[opt] = '\0';
while ((opt = getopt_long(argc, argv, opts, options, 0)) != -1)
{
switch (opt)
{
case 'g':
opt_lines = 0;
opt_columns = strtol(optarg, &end, 10);
if (*end == 'x' || *end == 'X') {
opt_lines = strtol(end + 1, &end, 10);
}
if (*end != '\0' || opt_lines <= 0 || opt_columns <= 0)
{
usage("geometry must be COLSxROWS");
}
printf("%s: version %s %s\n", me, conspy_version, conspy_date);
exit(0);
case 'V':
printf("%s: version %s %s\n", me, conspy_version, conspy_date);
exit(0);
case 'v':
opt_viewonly = 1;
break;
default:
usage(0);
}
}
virtual_console = argv[optind];
if (virtual_console != 0)
{
strtol(virtual_console, &end, 10);
if (*end != '\0' || end - virtual_console > 2)
usage("invalid virtual console \"%s\"", virtual_console);
}
/*
* Verify we can open the devices.
*/
strcpy(vcsa_name, "/dev/vcsa");
vcc_errno = 0;
if (virtual_console != 0)
strcat(vcsa_name, virtual_console);
strcpy(vcc_name, "/dev/vcc/a");
if (virtual_console != 0 && *virtual_console != '\0')
strcat(vcc_name, virtual_console);
else
strcat(vcc_name, "0");
strcpy(device_name, vcsa_name);
device_handle = open(vcsa_name, O_RDONLY);
vcsa_errno = errno;
if (device_handle == -1)
{
strcpy(device_name, vcc_name);
device_handle = open(vcc_name, O_RDONLY);
vcc_errno = errno;
}
if (device_handle == -1)
{
fprintf(stderr, "%s: could not open either the alternate device files.\n", me);
errno = vcsa_errno;
perror(vcsa_name);
errno = vcc_errno;
perror(vcc_name);
exit(1);
}
if (!opt_viewonly)
{
strcpy(tty_name, "/dev/tty");
if (virtual_console != 0 && *virtual_console != '\0')
strcat(tty_name, virtual_console);
else
strcat(tty_name, "0");
tty_handle = open(tty_name, O_WRONLY);
if (tty_handle == -1 && errno == ENOENT)
{
strcpy(tty_name, "/dev/vc/");
if (virtual_console != 0 && *virtual_console != '\0')
strcat(tty_name, virtual_console);
else
strcat(tty_name, "0");
tty_handle = open(tty_name, O_WRONLY);
}
if (tty_handle == -1)
{
perror(tty_name);
exit(1);
}
}
}
/*
* Initialise the Curses box char set. This must follow
*/
static void init_cursesbox()
{
cursesbox[IBM_BLOCK] = ACS_BLOCK;
cursesbox[IBM_BOARD] = ACS_BOARD;
cursesbox[IBM_BTEE] = ACS_BTEE;
cursesbox[IBM_CKBOARD] = ACS_CKBOARD;
cursesbox[IBM_DARROW] = ACS_DARROW;
cursesbox[IBM_DEGREE] = ACS_DEGREE;
cursesbox[IBM_GEQUAL] = ACS_GEQUAL;
cursesbox[IBM_HLINE] = ACS_HLINE;
cursesbox[IBM_LANTERN] = ACS_LANTERN;
cursesbox[IBM_LARROW] = ACS_LARROW;
cursesbox[IBM_LLCORNER] = ACS_LLCORNER;
cursesbox[IBM_LRCORNER] = ACS_LRCORNER;
cursesbox[IBM_LTEE] = ACS_LTEE;
cursesbox[IBM_PI] = ACS_PI;
cursesbox[IBM_PLMINUS] = ACS_PLMINUS;
cursesbox[IBM_RTEE] = ACS_RTEE;
cursesbox[IBM_STERLING] = ACS_STERLING;
cursesbox[IBM_TTEE] = ACS_TTEE;
cursesbox[IBM_UARROW] = ACS_UARROW;
cursesbox[IBM_ULCORNER] = ACS_ULCORNER;
cursesbox[IBM_URCORNER] = ACS_URCORNER;
cursesbox[IBM_VLINE] = ACS_VLINE;
}
/*
* Print an OS error and die.
*/
static void syserror(char* message, ...)
{
int errnr = errno;
va_list list;
cleanup();
va_start(list, message);
vfprintf(stderr, message, list);
va_end(list);
fprintf(stderr, ": %s.\n", strerror(errnr));
exit(1);
}
/*
* Allocate some memory.
*/
static void* checked_malloc(size_t bytes)
{
void* result = malloc(bytes);
if (result == 0)
syserror("memory allocation failed");
return result;
}
/*
* Die, possibly from a signal.
*/
static void finish(int sig)
{
sigset_t sigset;
cleanup();
if (sig <= 0)
exit(-sig);
sigemptyset(&sigset);
sigaddset(&sigset, sig);
sigprocmask(SIG_UNBLOCK, &sigset, 0);
signal(sig, SIG_DFL);
kill(getpid(), sig);
pause();
}
/*
* Set up curses and the TTY.
*/
static int setup()
{
int colour;
int background;
int foreground;
struct termios termios;
int use_colour;
/*
* Get a copy of the current TTY settings.
*/
if (tcgetattr(0, &old_termios) == -1)
{
perror("tcgetattr(0)");
exit(1);
}
/*
* Start curses.
*/
(void)signal(SIGHUP, finish);
(void)signal(SIGINT, finish);
(void)signal(SIGTERM, finish);
(void)initscr();
(void)nonl();
/*
* Set up the tty. All characters must be passed through to
* us unaltered.
*/
termios = old_termios;
termios.c_iflag &= ~(BRKINT|INLCR|ICRNL|IXON|IXOFF|IUCLC|IXANY|IMAXBEL);
termios.c_oflag &= ~(OPOST);
termios.c_lflag &= ~(ISIG|ICANON|ECHO);
if (tcsetattr(0, TCSANOW, &termios) == -1)
syserror("tcsetattr(0)");
/*
* Set up the colour map, if we can.
*/
use_colour = 0;
if (has_colors())
{
start_color();
if (COLOR_PAIRS >= 64)
use_colour = 1;
}
if (use_colour)
{
for (foreground = 0; foreground < 8; foreground += 1)
{
for (background = 0; background < 8; background += 1)
{
colour = VGA_PAIR(foreground, background);
if (colour != 0)
init_pair(colour, colour_map[foreground], colour_map[background]);
}
}
}
return use_colour;
}
/*
* Shut down curses, and restore everything.
*/
static void cleanup()
{
tcsetattr(0, TCSANOW, &old_termios);
endwin();
if (device_handle != -1)
close(device_handle);
if (tty_handle != -1)
close(tty_handle);
}
/*
* This is where the actual work is done.
*/
static void conspy(int use_colour)
{
unsigned short box;
size_t bytes_read;
unsigned int column;
attr_t curses_attribute;
short curses_colour;
int escape_pressed;
int escape_notpressed;
int ioerror_count;
unsigned int key_count;
unsigned int key_index;
uint32_t keyboard_mode;
char keys_pressed[256];
unsigned int last_attribute;
unsigned int last_columns;
unsigned int last_lines;
unsigned int line;
chtype line_buf[256];
int line_chars;
fd_set readset;
int result;
struct timeval timeval;
unsigned int curr_columns;
unsigned int curr_lines;
int tty_result;
unsigned int video_attribute;
int video_char;
struct vidbuf* vidbuf;
size_t vidbuf_size;
struct vidchar* vidchar;
curses_colour = 0;
curses_attribute = 0;
escape_notpressed = 0;
escape_pressed = 0;
ioerror_count = 0;
key_count = 0;
last_attribute = ~0U;
last_columns = 0;
last_lines = 0;
curr_columns = opt_columns ? opt_columns : 80;
curr_lines = opt_lines ? opt_lines : 25;
vidbuf_size = VIDBUF_SIZE(curr_columns, curr_lines) + sizeof(vidchar);
vidbuf = checked_malloc(vidbuf_size);
for (;;)
{
/*
* Read the video buffer.
*/
for (;;)
{
if (lseek(device_handle, 0L, SEEK_SET) != 0L)
syserror(device_name);
bytes_read = read(device_handle, vidbuf, vidbuf_size);
if (bytes_read < sizeof(*vidbuf) || bytes_read > vidbuf_size)
syserror(device_name);
if (bytes_read < vidbuf_size)
break;
vidbuf_size *= 2;
free(vidbuf);
vidbuf = checked_malloc(vidbuf_size);
}
if (bytes_read == VIDBUF_SIZE(opt_columns, opt_lines))
{
curr_columns = opt_columns;
curr_lines = opt_lines;
}
else
{
int i, j = -1, k = -1;
for (i = 0; i <= 7; i += 1)
{
curr_columns = vidbuf->vidbuf_columns + (i / 2 * 256);
curr_lines = vidbuf->vidbuf_lines + (i % 2 * 256);
if (bytes_read == VIDBUF_SIZE(curr_columns, curr_lines))
{
k = j;
j = i;
}
}
if (j == -1 || k != -1)
{
fprintf(stderr, "\nCan not guess the geometry of the console.\n");
exit(1);
}
curr_columns = vidbuf->vidbuf_columns + (j / 2 * 256);
curr_lines = vidbuf->vidbuf_lines + (j % 2 * 256);
}
/*
* If the screen size has changed blank out the unused portions.
*/
if (curr_lines < last_lines && last_lines < (unsigned)LINES)
{
move(curr_lines, 0);
clrtobot();
}
if (curr_columns < last_columns && last_columns < (unsigned)COLS)
{
for (line = 0; line < last_lines && line < (unsigned)LINES; line += 1)
{
move(line, last_columns);
clrtoeol();
}
}
last_lines = curr_lines;
last_columns = curr_columns;
/*
* Write the data to the screen.
*/
vidchar = vidbuf->vidbuf_chars;
for (line = 0; line < curr_lines && line < (unsigned)LINES; line += 1)
{
line_chars = 0;
for (column = 0; column < curr_columns; column += 1)
{
if (column >= (unsigned)COLS)
{
vidchar += curr_columns - column;
break;
}
video_attribute = VIDCHAR_ATTRIBUTE(vidchar);
video_char = VIDCHAR_CHAR(vidchar);
box = cursesbox[video_char];
if (box != 0)
{
video_attribute |= 0x100;
video_char = box;
}
if (video_char < ' ')
video_char = ' ';
if (video_attribute != last_attribute)
{
if (line_chars > 0)
{
move(line, column - line_chars);
addchnstr(line_buf, line_chars);
wchgat(stdscr, line_chars, curses_attribute, curses_colour, 0);
line_chars = 0;
}
curses_attribute = A_NORMAL;
if (video_attribute & 0x100)
curses_attribute |= A_ALTCHARSET;
if (video_attribute & 0x80)
curses_attribute |= A_BLINK;
if (video_attribute & 0x08)
curses_attribute |= A_BOLD;
if (use_colour)
{
curses_colour =
VGA_PAIR(video_attribute & 0x7, video_attribute>>4 & 0x7);
}
last_attribute = video_attribute;
}
line_buf[line_chars++] = video_char;
vidchar += 1;
}
move(line, column - line_chars);
addchnstr(line_buf, line_chars);
wchgat(stdscr, line_chars, curses_attribute, curses_colour, 0);
}
if (vidbuf->vidbuf_curline < LINES && vidbuf->vidbuf_curcolumn < COLS)
move(vidbuf->vidbuf_curline, vidbuf->vidbuf_curcolumn);
refresh();
/*
* Wait for 1/4 or a second, or for a character to be pressed.
*/
FD_ZERO(&readset);
FD_SET(0, &readset);
timeval.tv_sec = 0;
timeval.tv_usec = 250 * 1000L;
result = select(0 + 1, &readset, 0, 0, &timeval);
if (result == -1)
{
if (errno != EINTR)
syserror("select([tty_handle],0,0,timeval)");
endwin();
refresh();
continue;
}
/*
* Read the keys pressed.
*/
bytes_read = 0;
if (result == 1)
{
bytes_read =
read(0, keys_pressed + key_count, sizeof(keys_pressed) - key_count);
if (bytes_read == ~0U)
syserror(tty_name);
}
/*
* Do exit processing.
*/
if (result == 0 && ++escape_notpressed == 4)
{ /* >1sec since last key press */
escape_pressed = 0; /* That ends any exit sequence */
escape_notpressed = 0;
}
for (key_index = key_count; key_index < key_count+bytes_read; key_index += 1)
{ /* See if escape pressed 3 times */
if (keys_pressed[key_index] != '\033')
escape_pressed = 0;
else if (++escape_pressed == 3)
return;
if (keys_pressed[key_index] == ('L' & 0x1F))
wrefresh(curscr);
}
/*
* 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.
*/
if (!opt_viewonly)
{
/*
* Close & re-open tty in case they have swapped virtual consoles.
*/
close(tty_handle);
tty_handle = open(tty_name, O_WRONLY);
if (tty_handle == -1)
syserror(tty_name);
key_count += bytes_read;
tty_result = ioctl(tty_handle, KDGKBMODE, &keyboard_mode);
if (tty_result == -1)
;
else if (keyboard_mode != K_XLATE && keyboard_mode != K_UNICODE)
key_count = 0; /* Keyboard is in scan code mode */
else
{
for (key_index = 0; key_index < key_count; key_index += 1)
{
tty_result = ioctl(tty_handle, TIOCSTI, keys_pressed + key_index);
if (tty_result == -1)
break;
}
if (key_index == key_count) /* All keys sent? */
key_count = 0; /* Yes - clear the buffer */
else
{
memmove(keys_pressed, keys_pressed+key_index, key_count-key_index);
key_count -= key_index;
}
}
/*
* We sometimes get spurious IO errors on the TTY as programs
* close and re-open it. Usually they will just go away, if
* we are patient.
*/
if (tty_result != -1)
ioerror_count = 0;
else if (errno != EIO || ++ioerror_count > 4)
syserror(tty_name);
}
}
}