From 0a5bf7bb2840bb493f0f297c4dce86e2bda04cc9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 9 Jan 2012 01:55:55 +0200 Subject: [PATCH] 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 --- src/console.h | 3 + src/console_cell.c | 8 +- src/terminal.c | 92 ++++++++--- src/terminal.h | 13 +- src/vte.c | 364 +++++++++++++++++++++++++++++++++++++++++- src/vte.h | 16 +- tests/test_terminal.c | 20 ++- 7 files changed, 473 insertions(+), 43 deletions(-) diff --git a/src/console.h b/src/console.h index b91c625..d5d0d56 100644 --- a/src/console.h +++ b/src/console.h @@ -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, diff --git a/src/console_cell.c b/src/console_cell.c index e3608e0..485e7f8 100644 --- a/src/console_cell.c +++ b/src/console_cell.c @@ -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; diff --git a/src/terminal.c b/src/terminal.c index ec61bfd..c02b802 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -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); } diff --git a/src/terminal.h b/src/terminal.h index 0168b5f..3ce04d6 100644 --- a/src/terminal.h +++ b/src/terminal.h @@ -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 */ diff --git a/src/vte.c b/src/vte.c index 105e501..4c4578f 100644 --- a/src/vte.c +++ b/src/vte.c @@ -30,11 +30,22 @@ * console subsystem as output and is tightly bound to it. */ +/* for pty functions */ +#define _XOPEN_SOURCE 700 + +#include #include +#include +#include +#include #include #include +#include +#include #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); +} diff --git a/src/vte.h b/src/vte.h index 1d82752..30296c9 100644 --- a/src/vte.h +++ b/src/vte.h @@ -36,14 +36,26 @@ #include #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 */ diff --git a/tests/test_terminal.c b/tests/test_terminal.c index 48d30ba..19c20f0 100644 --- a/tests/test_terminal.c +++ b/tests/test_terminal.c @@ -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;