vte, terminal: add pseudo terminal support

This commit adds some needed terminal emulator infrastructure.

We allow to "open" (and close) a vte object (resp. terminal object).
We then create a pty pair, fork and exec a shell. We route input to the
shell and draw its output to the console.

We add callbacks for when
- The buffer changes (through the vte object). We can then schedule a
  screen redraw.
- The shell (child process) exits. We can then exit ourselves, start up
  a new shell, etc.

There is not yet any real VTE processing, so we display raw escape
codes and so on. However, this should provide immediate feedback for
any further vte development, as we start to act like a real terminal
emulator.

Signed-off-by: Ran Benita <ran234@gmail.com>
This commit is contained in:
Ran Benita 2012-01-09 01:55:55 +02:00
parent 969da54ee3
commit 0a5bf7bb28
7 changed files with 473 additions and 43 deletions

View File

@ -43,6 +43,9 @@
struct kmscon_buffer;
struct kmscon_console;
#define KMSCON_DEFAULT_WIDTH 80
#define KMSCON_DEFAULT_HEIGHT 24
/* console buffer with cell objects */
int kmscon_buffer_new(struct kmscon_buffer **out, unsigned int x,

View File

@ -86,8 +86,6 @@
#include "log.h"
#include "unicode.h"
#define DEFAULT_WIDTH 80
#define DEFAULT_HEIGHT 24
#define DEFAULT_SCROLLBACK 128
struct cell {
@ -172,7 +170,7 @@ static int resize_line(struct line *line, unsigned int width)
return -EINVAL;
if (!width)
width = DEFAULT_WIDTH;
width = KMSCON_DEFAULT_WIDTH;
if (line->size < width) {
tmp = realloc(line->cells, width * sizeof(struct cell));
@ -337,9 +335,9 @@ int kmscon_buffer_resize(struct kmscon_buffer *buf, unsigned int x,
return -EINVAL;
if (!x)
x = DEFAULT_WIDTH;
x = KMSCON_DEFAULT_WIDTH;
if (!y)
y = DEFAULT_HEIGHT;
y = KMSCON_DEFAULT_HEIGHT;
if (buf->size_x == x && buf->size_y == y)
return 0;

View File

@ -39,6 +39,7 @@
#include "console.h"
#include "eloop.h"
#include "font.h"
#include "input.h"
#include "log.h"
#include "terminal.h"
#include "unicode.h"
@ -59,6 +60,9 @@ struct kmscon_terminal {
struct kmscon_console *console;
struct kmscon_idle *redraw;
struct kmscon_vte *vte;
kmscon_terminal_closed_cb closed_cb;
void *closed_data;
};
static void draw_all(struct kmscon_idle *idle, void *data)
@ -101,20 +105,10 @@ static void schedule_redraw(struct kmscon_terminal *term)
log_warning("terminal: cannot schedule redraw\n");
}
static const char help_text[] =
"terminal subsystem - KMS based console test\n"
"This is some default text to test the drawing operations.\n\n";
static void print_help(struct kmscon_terminal *term)
void vte_changed(struct kmscon_vte *vte, void *data)
{
unsigned int i, len;
kmscon_symbol_t ch;
len = sizeof(help_text) - 1;
for (i = 0; i < len; ++i) {
ch = kmscon_symbol_make(help_text[i]);
kmscon_terminal_input(term, ch);
}
struct kmscon_terminal *term = data;
schedule_redraw(term);
}
int kmscon_terminal_new(struct kmscon_terminal **out,
@ -143,11 +137,10 @@ int kmscon_terminal_new(struct kmscon_terminal **out,
if (ret)
goto err_idle;
ret = kmscon_vte_new(&term->vte);
ret = kmscon_vte_new(&term->vte, vte_changed, term);
if (ret)
goto err_con;
kmscon_vte_bind(term->vte, term->console);
print_help(term);
*out = term;
return 0;
@ -177,16 +170,16 @@ void kmscon_terminal_unref(struct kmscon_terminal *term)
if (--term->ref)
return;
term->closed_cb = NULL;
kmscon_terminal_close(term);
kmscon_terminal_rm_all_outputs(term);
kmscon_vte_unref(term->vte);
kmscon_console_unref(term->console);
kmscon_terminal_disconnect_eloop(term);
free(term);
log_debug("terminal: destroying terminal object\n");
}
int kmscon_terminal_connect_eloop(struct kmscon_terminal *term,
struct kmscon_eloop *eloop)
int connect_eloop(struct kmscon_terminal *term, struct kmscon_eloop *eloop)
{
if (!term || !eloop)
return -EINVAL;
@ -200,7 +193,7 @@ int kmscon_terminal_connect_eloop(struct kmscon_terminal *term,
return 0;
}
void kmscon_terminal_disconnect_eloop(struct kmscon_terminal *term)
void disconnect_eloop(struct kmscon_terminal *term)
{
if (!term)
return;
@ -209,6 +202,60 @@ void kmscon_terminal_disconnect_eloop(struct kmscon_terminal *term)
term->eloop = NULL;
}
static void vte_closed(struct kmscon_vte *vte, void *data)
{
struct kmscon_terminal *term = data;
kmscon_terminal_close(term);
}
int kmscon_terminal_open(struct kmscon_terminal *term,
struct kmscon_eloop *eloop,
kmscon_terminal_closed_cb closed_cb, void *data)
{
int ret;
if (!term)
return -EINVAL;
ret = connect_eloop(term, eloop);
if (ret == -EALREADY) {
disconnect_eloop(term);
ret = connect_eloop(term, eloop);
}
if (ret)
return ret;
ret = kmscon_vte_open(term->vte, eloop, vte_closed, term);
if (ret) {
disconnect_eloop(term);
return ret;
}
term->closed_cb = closed_cb;
term->closed_data = data;
return 0;
}
void kmscon_terminal_close(struct kmscon_terminal *term)
{
kmscon_terminal_closed_cb cb;
void *data;
if (!term)
return;
cb = term->closed_cb;
data = term->closed_data;
term->closed_data = NULL;
term->closed_cb = NULL;
disconnect_eloop(term);
kmscon_vte_close(term->vte);
if (cb)
cb(term, data);
}
int kmscon_terminal_add_output(struct kmscon_terminal *term,
struct kmscon_output *output)
{
@ -239,6 +286,7 @@ int kmscon_terminal_add_output(struct kmscon_terminal *term,
if (term->max_height < height) {
term->max_height = height;
kmscon_console_resize(term->console, 0, 0, term->max_height);
kmscon_vte_resize(term->vte);
}
schedule_redraw(term);
@ -261,8 +309,8 @@ void kmscon_terminal_rm_all_outputs(struct kmscon_terminal *term)
}
}
void kmscon_terminal_input(struct kmscon_terminal *term, kmscon_symbol_t ch)
void kmscon_terminal_input(struct kmscon_terminal *term,
struct kmscon_input_event *ev)
{
kmscon_vte_input(term->vte, ch);
schedule_redraw(term);
kmscon_vte_input(term->vte, ev);
}

View File

@ -41,19 +41,24 @@
struct kmscon_terminal;
typedef void (*kmscon_terminal_closed_cb) (struct kmscon_terminal *term,
void *data);
int kmscon_terminal_new(struct kmscon_terminal **out,
struct kmscon_font_factory *ff);
void kmscon_terminal_ref(struct kmscon_terminal *term);
void kmscon_terminal_unref(struct kmscon_terminal *term);
int kmscon_terminal_connect_eloop(struct kmscon_terminal *term,
struct kmscon_eloop *eloop);
void kmscon_terminal_disconnect_eloop(struct kmscon_terminal *term);
int kmscon_terminal_open(struct kmscon_terminal *term,
struct kmscon_eloop *eloop,
kmscon_terminal_closed_cb closed_cb, void *data);
void kmscon_terminal_close(struct kmscon_terminal *term);
int kmscon_terminal_add_output(struct kmscon_terminal *term,
struct kmscon_output *output);
void kmscon_terminal_rm_all_outputs(struct kmscon_terminal *term);
void kmscon_terminal_input(struct kmscon_terminal *term, kmscon_symbol_t ch);
void kmscon_terminal_input(struct kmscon_terminal *term,
struct kmscon_input_event *ev);
#endif /* KMSCON_TERMINAL_H */

364
src/vte.c
View File

@ -30,11 +30,22 @@
* console subsystem as output and is tightly bound to it.
*/
/* for pty functions */
#define _XOPEN_SOURCE 700
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <paths.h>
#include <pty.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <X11/keysym.h>
#include "console.h"
#include "eloop.h"
#include "input.h"
#include "log.h"
#include "unicode.h"
#include "vte.h"
@ -42,9 +53,20 @@
struct kmscon_vte {
unsigned long ref;
struct kmscon_console *con;
struct kmscon_eloop *eloop;
int pty;
struct kmscon_fd *pty_fd;
kmscon_vte_changed_cb changed_cb;
void *changed_data;
kmscon_vte_closed_cb closed_cb;
void *closed_data;
};
int kmscon_vte_new(struct kmscon_vte **out)
int kmscon_vte_new(struct kmscon_vte **out,
kmscon_vte_changed_cb changed_cb, void *data)
{
struct kmscon_vte *vte;
@ -58,8 +80,11 @@ int kmscon_vte_new(struct kmscon_vte **out)
return -ENOMEM;
memset(vte, 0, sizeof(*vte));
vte->pty = -1;
vte->ref = 1;
vte->changed_cb = changed_cb;
vte->changed_data = data;
*out = vte;
return 0;
}
@ -81,6 +106,7 @@ void kmscon_vte_unref(struct kmscon_vte *vte)
return;
kmscon_console_unref(vte->con);
kmscon_vte_close(vte);
free(vte);
log_debug("vte: destroying vte object\n");
}
@ -95,7 +121,37 @@ void kmscon_vte_bind(struct kmscon_vte *vte, struct kmscon_console *con)
kmscon_console_ref(vte->con);
}
void kmscon_vte_input(struct kmscon_vte *vte, kmscon_symbol_t ch)
/* FIXME: this is just temporary. */
void kmscon_vte_input(struct kmscon_vte *vte, struct kmscon_input_event *ev)
{
kmscon_symbol_t ch;
ssize_t len;
if (!vte || !vte->con || vte->pty < 0)
return;
if (ev->keysym == XK_Return)
ch = '\n';
else if (ev->unicode == KMSCON_INPUT_INVALID)
return;
else
ch = kmscon_symbol_make(ev->unicode);
if (ch > 127)
return;
if (ev->mods & KMSCON_CONTROL_MASK)
if (iscntrl(toupper(ch) ^ 64))
ch = toupper(ch) ^ 64;
len = write(vte->pty, (char *)&ch, 1);
if (len <= 0) {
kmscon_vte_close(vte);
return;
}
}
void kmscon_vte_putc(struct kmscon_vte *vte, kmscon_symbol_t ch)
{
if (!vte || !vte->con)
return;
@ -105,3 +161,307 @@ void kmscon_vte_input(struct kmscon_vte *vte, kmscon_symbol_t ch)
else
kmscon_console_write(vte->con, ch);
}
/*
* TODO:
* - Decide which terminal we're emulating and set TERM accordingly.
* - Decide what to exec here: login, some getty equivalent, a shell...
* - Might also need to update some details in utmp wtmp and friends.
*/
static void __attribute__((noreturn))
exec_child(int pty_master)
{
const char *sh;
setenv("TERM", "linux", 1);
sh = getenv("SHELL") ?: _PATH_BSHELL;
execlp(sh, sh, "-i", NULL);
_exit(EXIT_FAILURE);
}
static int fork_pty_child(int master, struct winsize *ws)
{
int ret, saved_errno;
pid_t pid;
const char *slave_name;
int slave = -1;
/* This doesn't actually do anything on linux. */
ret = grantpt(master);
if (ret < 0) {
log_err("vte: grantpt failed: %m");
goto err_out;
}
ret = unlockpt(master);
if (ret < 0) {
log_err("vte: cannot unlock pty: %m");
goto err_out;
}
slave_name = ptsname(master);
if (!slave_name) {
log_err("vte: cannot find pty slave name: %m");
goto err_out;
}
/* This also loses our controlling tty. */
pid = setsid();
if (pid < 0) {
log_err("vte: cannot start a new session: %m");
goto err_out;
}
/* And the slave pty becomes our controlling tty. */
slave = open(slave_name, O_RDWR | O_CLOEXEC);
if (slave < 0) {
log_err("vte: cannot open pty slave: %m");
goto err_out;
}
ret = ioctl(slave, TIOCSWINSZ, ws);
if (ret)
log_warning("vte: cannot set slave pty window size: %m");
if (dup2(slave, STDIN_FILENO) != STDIN_FILENO ||
dup2(slave, STDOUT_FILENO) != STDOUT_FILENO ||
dup2(slave, STDERR_FILENO) != STDERR_FILENO) {
log_err("vte: cannot duplicate slave pty: %m");
goto err_out;
}
close(master);
close(slave);
return 0;
err_out:
saved_errno = errno;
if (slave > 0)
close(slave);
close(master);
return -saved_errno;
}
/*
* This is functionally equivalent to forkpty(3). We do it manually to obtain
* a little bit more control of the process, and as a bonus avoid linking to
* the libutil library in glibc.
*/
static pid_t fork_pty(int *pty_out, struct winsize *ws)
{
int ret;
pid_t pid;
int master;
master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC | O_NONBLOCK);
if (master < 0) {
ret = -errno;
log_err("vte: cannot open pty master: %m");
goto err_out;
}
pid = fork();
switch (pid) {
case -1:
log_err("vte: failed to fork pty slave: %m");
ret = -errno;
goto err_master;
case 0:
ret = fork_pty_child(master, ws);
if (ret)
goto err_master;
*pty_out = -1;
return 0;
default:
*pty_out = master;
return pid;
}
err_master:
close(master);
err_out:
*pty_out = -1;
errno = -ret;
return -1;
}
static int pty_spawn(struct kmscon_vte *vte)
{
pid_t pid;
if (vte->pty >= 0)
return -EALREADY;
struct winsize ws;
memset(&ws, 0, sizeof(ws));
ws.ws_col = kmscon_console_get_width(vte->con) ?:
KMSCON_DEFAULT_WIDTH;
ws.ws_row = kmscon_console_get_height(vte->con) ?:
KMSCON_DEFAULT_HEIGHT;
pid = fork_pty(&vte->pty, &ws);
switch (pid) {
case -1:
log_err("vte: cannot fork or open pty pair: %m");
return -errno;
case 0:
exec_child(vte->pty);
default:
break;
}
return 0;
}
void pty_input(struct kmscon_fd *fd, int mask, void *data)
{
int ret, nread;
ssize_t len, i;
struct kmscon_vte *vte = data;
if (!vte || vte->pty < 0)
return;
/*
* If we get a hangup or an error, but the pty is still readable, we
* read what's left and deal with the rest on the next dispatch.
*/
if (!(mask & KMSCON_READABLE)) {
if (mask & KMSCON_ERR)
log_warning("vte: error condition happened on pty\n");
kmscon_vte_close(vte);
return;
}
ret = ioctl(vte->pty, FIONREAD, &nread);
if (ret) {
log_warning("vte: cannot peek into pty input buffer: %m");
return;
} else if (nread <= 0) {
return;
}
char buf[nread];
len = read(vte->pty, buf, nread);
if (len == -1) {
/* EIO is hangup, although we should have caught it above. */
if (errno != EIO)
log_err("vte: cannot read from pty: %m");
kmscon_vte_close(vte);
return;
} else if (len == 0) {
kmscon_vte_close(vte);
return;
}
for (i=0; i < len; i++)
kmscon_vte_putc(vte, buf[i]);
if (vte->changed_cb)
vte->changed_cb(vte, vte->changed_data);
}
static int connect_eloop(struct kmscon_vte *vte, struct kmscon_eloop *eloop)
{
int ret;
if (vte->eloop)
return -EALREADY;
ret = kmscon_eloop_new_fd(eloop, &vte->pty_fd, vte->pty,
KMSCON_READABLE, pty_input, vte);
if (ret)
return ret;
kmscon_eloop_ref(eloop);
vte->eloop = eloop;
return 0;
}
static void disconnect_eloop(struct kmscon_vte *vte)
{
kmscon_eloop_rm_fd(vte->pty_fd);
kmscon_eloop_unref(vte->eloop);
vte->pty_fd = NULL;
vte->eloop = NULL;
}
int kmscon_vte_open(struct kmscon_vte *vte, struct kmscon_eloop *eloop,
kmscon_vte_closed_cb closed_cb, void *data)
{
int ret;
if (!vte || !eloop)
return -EINVAL;
if (vte->pty >= 0)
return -EALREADY;
ret = pty_spawn(vte);
if (ret)
return ret;
ret = connect_eloop(vte, eloop);
if (ret == -EALREADY) {
disconnect_eloop(vte);
ret = connect_eloop(vte, eloop);
}
if (ret) {
close(vte->pty);
vte->pty = -1;
return ret;
}
vte->closed_cb = closed_cb;
vte->closed_data = data;
return 0;
}
void kmscon_vte_close(struct kmscon_vte *vte)
{
kmscon_vte_closed_cb cb;
void *data;
if (!vte || vte->pty < 0)
return;
disconnect_eloop(vte);
close(vte->pty);
vte->pty = -1;
cb = vte->closed_cb;
data = vte->closed_data;
vte->closed_cb = NULL;
vte->closed_data = NULL;
if (cb)
cb(vte, data);
}
void kmscon_vte_resize(struct kmscon_vte *vte)
{
int ret;
struct winsize ws;
if (!vte || !vte->con || vte->pty < 0)
return;
memset(&ws, 0, sizeof(ws));
ws.ws_col = kmscon_console_get_width(vte->con);
ws.ws_row = kmscon_console_get_height(vte->con);
/*
* This will send SIGWINCH to the pty slave foreground process group.
* We will also get one, but we don't need it.
*/
ret = ioctl(vte->pty, TIOCSWINSZ, &ws);
if (ret) {
log_warning("vte: cannot set window size\n");
return;
}
log_debug("vte: window size set to %hdx%hd\n", ws.ws_col, ws.ws_row);
}

View File

@ -36,14 +36,26 @@
#include <stdlib.h>
#include "console.h"
#include "unicode.h"
#include "eloop.h"
struct kmscon_vte;
int kmscon_vte_new(struct kmscon_vte **out);
typedef void (*kmscon_vte_changed_cb) (struct kmscon_vte *vte, void *data);
typedef void (*kmscon_vte_closed_cb) (struct kmscon_vte *vte, void *data);
int kmscon_vte_new(struct kmscon_vte **out,
kmscon_vte_changed_cb changed_cb, void *data);
void kmscon_vte_ref(struct kmscon_vte *vte);
void kmscon_vte_unref(struct kmscon_vte *vte);
int kmscon_vte_open(struct kmscon_vte *vte, struct kmscon_eloop *eloop,
kmscon_vte_closed_cb closed_cb, void *data);
void kmscon_vte_close(struct kmscon_vte *vte);
void kmscon_vte_bind(struct kmscon_vte *vte, struct kmscon_console *con);
void kmscon_vte_input(struct kmscon_vte *vte, kmscon_symbol_t ch);
void kmscon_vte_resize(struct kmscon_vte *vte);
void kmscon_vte_input(struct kmscon_vte *vte, struct kmscon_input_event *ev);
void kmscon_vte_putc(struct kmscon_vte *vte, kmscon_symbol_t ch);
#endif /* KMSCON_VTE_H */

View File

@ -63,17 +63,20 @@ static void sig_term(struct kmscon_signal *sig, int signum, void *data)
terminate = 1;
}
static void terminal_closed(struct kmscon_terminal *term, void *data)
{
/*
* Alternativly, we could spwan a new login/shell here, like what
* happens when the user exits the shell in a linux console.
*/
terminate = 1;
}
static void read_input(struct kmscon_input *input,
struct kmscon_input_event *ev, void *data)
{
struct app *app = data;
kmscon_symbol_t ch;
if (ev->unicode == KMSCON_INPUT_INVALID)
return;
ch = kmscon_symbol_make(ev->unicode);
kmscon_terminal_input(app->term, ch);
kmscon_terminal_input(app->term, ev);
}
static void activate_outputs(struct app *app)
@ -186,7 +189,8 @@ static int setup_app(struct app *app)
if (ret)
goto err_loop;
ret = kmscon_terminal_connect_eloop(app->term, app->eloop);
ret = kmscon_terminal_open(app->term, app->eloop,
terminal_closed, NULL);
if (ret)
goto err_loop;