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..1e26f15 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,310 @@ 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; + } + + if (ws) { + 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..49b9467 100644 --- a/tests/test_terminal.c +++ b/tests/test_terminal.c @@ -36,6 +36,7 @@ #include #include #include +#include #include "eloop.h" #include "input.h" @@ -48,6 +49,7 @@ struct app { struct kmscon_eloop *eloop; struct kmscon_signal *sig_term; struct kmscon_signal *sig_int; + struct kmscon_signal *sig_chld; struct kmscon_symbol_table *st; struct kmscon_font_factory *ff; struct kmscon_compositor *comp; @@ -63,17 +65,68 @@ static void sig_term(struct kmscon_signal *sig, int signum, void *data) terminate = 1; } +static void sig_chld(struct kmscon_signal *sig, int signum, void *data) +{ + pid_t pid; + int status; + + /* + * If multiple children exit at the same time, signalfd would put them + * all in one event. So we reap in a loop. + */ + while (1) { + pid = waitpid(-1, &status, WNOHANG); + if (pid == -1) { + if (errno != ECHILD) + log_warning("vte: cannot wait on child: %m\n"); + break; + } else if (pid == 0) { + break; + } else if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) + log_info("vte: child %d exited with status " + "%hd\n", pid, WEXITSTATUS(status)); + else + log_debug("vte: child %d exited " + "successfully\n", pid); + } else if (WIFSIGNALED(status)) { + log_debug("vte: child %d exited by signal %d\n", pid, + WTERMSIG(status)); + } + } +} + +static void terminal_closed(struct kmscon_terminal *term, void *data) +{ +#if 0 + /* + * Alternativly, we could spwan a new login/shell here, like what + * happens when the user exits the shell in a linux console: + */ + + int ret; + struct app *app = data; + + if (!app) + goto err_out; + + ret = kmscon_terminal_open(app->term, app->eloop, + terminal_closed, app); + if (ret) + goto err_out; + + return; + +err_out: +#endif + 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) @@ -131,6 +184,7 @@ static void destroy_app(struct app *app) kmscon_compositor_unref(app->comp); kmscon_font_factory_unref(app->ff); kmscon_symbol_table_unref(app->st); + kmscon_eloop_rm_signal(app->sig_chld); kmscon_eloop_rm_signal(app->sig_int); kmscon_eloop_rm_signal(app->sig_term); kmscon_eloop_unref(app->eloop); @@ -154,6 +208,11 @@ static int setup_app(struct app *app) if (ret) goto err_loop; + ret = kmscon_eloop_new_signal(app->eloop, &app->sig_chld, SIGCHLD, + sig_chld, NULL); + if (ret) + goto err_loop; + ret = kmscon_symbol_table_new(&app->st); if (ret) goto err_loop; @@ -186,7 +245,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, app); if (ret) goto err_loop;