diff --git a/Makefile.am b/Makefile.am index 21ca61e..0c52720 100644 --- a/Makefile.am +++ b/Makefile.am @@ -31,7 +31,8 @@ libkmscon_core_la_SOURCES = \ src/input_xkb.c src/input_xkb.h \ external/imKStoUCS.c external\imKStoUCS.h \ src/vte.c src/vte.h \ - src/terminal.c src/terminal.h + src/terminal.c src/terminal.h \ + src/pty.c src/pty.h if USE_PANGO libkmscon_core_la_SOURCES += \ diff --git a/src/pty.c b/src/pty.c new file mode 100644 index 0000000..4f53170 --- /dev/null +++ b/src/pty.c @@ -0,0 +1,421 @@ +/* + * kmscon - Pseudo Terminal Handling + * + * Copyright (c) 2012 Ran Benita + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* for pty functions */ +#define _XOPEN_SOURCE 700 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "pty.h" + +struct kmscon_pty { + unsigned long ref; + struct kmscon_eloop *eloop; + + int fd; + struct kmscon_fd *efd; + + kmscon_pty_output_cb output_cb; + void *output_data; + + kmscon_pty_closed_cb closed_cb; + void *closed_data; +}; + +int kmscon_pty_new(struct kmscon_pty **out, + kmscon_pty_output_cb output_cb, void *data) +{ + struct kmscon_pty *pty; + + if (!out) + return -EINVAL; + + log_debug("pty: new pty object\n"); + + pty = malloc(sizeof(*pty)); + if (!pty) + return -ENOMEM; + + memset(pty, 0, sizeof(*pty)); + pty->fd = -1; + pty->ref = 1; + + pty->output_cb = output_cb; + pty->output_data = data; + *out = pty; + return 0; +} + +void kmscon_pty_ref(struct kmscon_pty *pty) +{ + if (!pty) + return; + + pty->ref++; +} + +void kmscon_pty_unref(struct kmscon_pty *pty) +{ + if (!pty || !pty->ref) + return; + + if (--pty->ref) + return; + + kmscon_pty_close(pty); + free(pty); + log_debug("pty: destroying pty object\n"); +} + +/* + * 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); + + log_err("pty: failed to exec child: %m\n"); + + _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("pty: grantpt failed: %m"); + goto err_out; + } + + ret = unlockpt(master); + if (ret < 0) { + log_err("pty: cannot unlock pty: %m"); + goto err_out; + } + + slave_name = ptsname(master); + if (!slave_name) { + log_err("pty: cannot find slave name: %m"); + goto err_out; + } + + /* This also loses our controlling tty. */ + pid = setsid(); + if (pid < 0) { + log_err("pty: 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("pty: cannot open slave: %m"); + goto err_out; + } + + if (ws) { + ret = ioctl(slave, TIOCSWINSZ, ws); + if (ret) + log_warning("pty: cannot set slave window size: %m"); + } + + if (dup2(slave, STDIN_FILENO) != STDIN_FILENO || + dup2(slave, STDOUT_FILENO) != STDOUT_FILENO || + dup2(slave, STDERR_FILENO) != STDERR_FILENO) { + log_err("pty: cannot duplicate slave: %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("pty: cannot open master: %m"); + goto err_out; + } + + pid = fork(); + switch (pid) { + case -1: + log_err("pty: cannot fork: %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_pty *pty, + unsigned short width, unsigned short height) +{ + struct winsize ws; + pid_t pid; + + if (pty->fd >= 0) + return -EALREADY; + + memset(&ws, 0, sizeof(ws)); + ws.ws_col = width; + ws.ws_row = height; + + pid = fork_pty(&pty->fd, &ws); + switch (pid) { + case -1: + log_err("pty: cannot fork or open pty pair: %m"); + return -errno; + case 0: + exec_child(pty->fd); + default: + break; + } + + return 0; +} + +static void pty_output(struct kmscon_fd *fd, int mask, void *data) +{ + int ret, nread; + ssize_t len; + struct kmscon_pty *pty = data; + + if (!pty || pty->fd < 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("pty: error condition happened on pty\n"); + kmscon_pty_close(pty); + return; + } + + ret = ioctl(pty->fd, FIONREAD, &nread); + if (ret) { + log_warning("pty: cannot peek into pty input buffer: %m"); + return; + } else if (nread <= 0) { + return; + } + + char u8[nread]; + len = read(pty->fd, u8, nread); + if (len == -1) { + if (errno == EWOULDBLOCK) + return; + /* EIO is hangup, although we should have caught it above. */ + if (errno != EIO) + log_err("pty: cannot read from pty: %m"); + kmscon_pty_close(pty); + return; + } else if (len == 0) { + kmscon_pty_close(pty); + return; + } + + if (pty->output_cb) + pty->output_cb(pty, u8, len, pty->output_data); +} + +static int connect_eloop(struct kmscon_pty *pty, struct kmscon_eloop *eloop) +{ + int ret; + + if (pty->eloop) + return -EALREADY; + + ret = kmscon_eloop_new_fd(eloop, &pty->efd, pty->fd, + KMSCON_READABLE, pty_output, pty); + if (ret) + return ret; + + kmscon_eloop_ref(eloop); + pty->eloop = eloop; + return 0; +} + +static void disconnect_eloop(struct kmscon_pty *pty) +{ + kmscon_eloop_rm_fd(pty->efd); + kmscon_eloop_unref(pty->eloop); + pty->efd = NULL; + pty->eloop = NULL; +} + +int kmscon_pty_open(struct kmscon_pty *pty, struct kmscon_eloop *eloop, + unsigned short width, unsigned short height, + kmscon_pty_closed_cb closed_cb, void *data) +{ + int ret; + + if (!pty || !eloop) + return -EINVAL; + + if (pty->fd >= 0) + return -EALREADY; + + ret = pty_spawn(pty, width, height); + if (ret) + return ret; + + ret = connect_eloop(pty, eloop); + if (ret == -EALREADY) { + disconnect_eloop(pty); + ret = connect_eloop(pty, eloop); + } + if (ret) { + close(pty->fd); + pty->fd = -1; + return ret; + } + + pty->closed_cb = closed_cb; + pty->closed_data = data; + return 0; +} + +void kmscon_pty_close(struct kmscon_pty *pty) +{ + kmscon_pty_closed_cb cb; + void *data; + + if (!pty || pty->fd < 0) + return; + + disconnect_eloop(pty); + + close(pty->fd); + pty->fd = -1; + + cb = pty->closed_cb; + data = pty->closed_data; + pty->closed_cb = NULL; + pty->closed_data = NULL; + + if (cb) + cb(pty, data); +} + +void kmscon_pty_input(struct kmscon_pty *pty, const char *u8, size_t len) +{ + if (!pty || pty->fd < 0) + return; + + /* FIXME: In EWOULDBLOCK we would lose input! Need to buffer. */ + len = write(pty->fd, u8, len); + if (len <= 0) { + if (errno != EWOULDBLOCK) + kmscon_pty_close(pty); + return; + } +} + +void kmscon_pty_resize(struct kmscon_pty *pty, + unsigned short width, unsigned short height) +{ + int ret; + struct winsize ws; + + if (!pty || pty->fd < 0) + return; + + memset(&ws, 0, sizeof(ws)); + ws.ws_col = width; + ws.ws_row = height; + + /* + * This will send SIGWINCH to the pty slave foreground process group. + * We will also get one, but we don't need it. + */ + ret = ioctl(pty->fd, TIOCSWINSZ, &ws); + if (ret) { + log_warning("pty: cannot set window size\n"); + return; + } + + log_debug("pty: window size set to %hdx%hd\n", ws.ws_col, ws.ws_row); +} diff --git a/src/pty.h b/src/pty.h new file mode 100644 index 0000000..6cb0877 --- /dev/null +++ b/src/pty.h @@ -0,0 +1,72 @@ +/* + * kmscon - Pseudo Terminal Handling + * + * Copyright (c) 2012 Ran Benita + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The pty object provides an interface for communicating with a child process + * over a pseudo terminal. The child is the host, we act as the TTY terminal, + * and the kernel is the driver. + * + * To use this, create a new pty object and open it. You will start receiving + * output notifications through the output_cb callback. To communicate with + * the other end of the terminal, use the kmscon_pty_input method. All + * communication is done using byte streams (presumably UTF-8). + * + * The pty can be closed voluntarily using the kmson_pty_close method. The + * child process can also exit at will; this will be communicated through the + * closed_cb callback. The pty object does not wait on the child processes it + * spawns; this is the responsibility of the object's user. + */ + +#ifndef KMSCON_PTY_H +#define KMSCON_PTY_H + +#include "eloop.h" + +struct kmscon_pty; + +typedef void (*kmscon_pty_output_cb) + (struct kmscon_pty *pty, char *u8, size_t len, void *data); +typedef void (*kmscon_pty_closed_cb) (struct kmscon_pty *pty, void *data); + +int kmscon_pty_new(struct kmscon_pty **out, + kmscon_pty_output_cb output_cb, void *data); +void kmscon_pty_ref(struct kmscon_pty *pty); +void kmscon_pty_unref(struct kmscon_pty *pty); + +int kmscon_pty_open(struct kmscon_pty *pty, struct kmscon_eloop *eloop, + unsigned short width, unsigned short height, + kmscon_pty_closed_cb closed_cb, void *data); +void kmscon_pty_close(struct kmscon_pty *pty); + +void kmscon_pty_input(struct kmscon_pty *pty, const char *u8, size_t len); + +/* + * Call this whenever the size of the screen (rows or columns) changes. The + * kernel and child process need to be notified. + */ +void kmscon_pty_resize(struct kmscon_pty *pty, + unsigned short width, unsigned short height); + +#endif /* KMSCON_PTY_H */ diff --git a/src/terminal.c b/src/terminal.c index ec61bfd..5dfbbcb 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -39,7 +39,9 @@ #include "console.h" #include "eloop.h" #include "font.h" +#include "input.h" #include "log.h" +#include "pty.h" #include "terminal.h" #include "unicode.h" #include "vte.h" @@ -59,6 +61,10 @@ struct kmscon_terminal { struct kmscon_console *console; struct kmscon_idle *redraw; struct kmscon_vte *vte; + struct kmscon_pty *pty; + + kmscon_terminal_closed_cb closed_cb; + void *closed_data; }; static void draw_all(struct kmscon_idle *idle, void *data) @@ -101,20 +107,17 @@ 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) +static void pty_output(struct kmscon_pty *pty, char *u8, size_t len, void *data) { - unsigned int i, len; - kmscon_symbol_t ch; + size_t i; + struct kmscon_terminal *term = data; - len = sizeof(help_text) - 1; - for (i = 0; i < len; ++i) { - ch = kmscon_symbol_make(help_text[i]); - kmscon_terminal_input(term, ch); - } + /* FIXME: UTF-8. */ + for (i=0; i < len; i++) + if (u8[i] < 128) + kmscon_vte_input(term->vte, u8[i]); + + schedule_redraw(term); } int kmscon_terminal_new(struct kmscon_terminal **out, @@ -147,11 +150,16 @@ int kmscon_terminal_new(struct kmscon_terminal **out, if (ret) goto err_con; kmscon_vte_bind(term->vte, term->console); - print_help(term); + + ret = kmscon_pty_new(&term->pty, pty_output, term); + if (ret) + goto err_vte; *out = term; return 0; +err_vte: + kmscon_vte_unref(term->vte); err_con: kmscon_console_unref(term->console); err_idle: @@ -177,16 +185,17 @@ 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_pty_unref(term->pty); 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 +209,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 +218,63 @@ void kmscon_terminal_disconnect_eloop(struct kmscon_terminal *term) term->eloop = NULL; } +static void pty_closed(struct kmscon_pty *pty, 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; + unsigned short width, height; + + if (!term) + return -EINVAL; + + ret = connect_eloop(term, eloop); + if (ret == -EALREADY) { + disconnect_eloop(term); + ret = connect_eloop(term, eloop); + } + if (ret) + return ret; + + width = kmscon_console_get_width(term->console); + height = kmscon_console_get_height(term->console); + ret = kmscon_pty_open(term->pty, eloop, width, height, pty_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_pty_close(term->pty); + + if (cb) + cb(term, data); +} + int kmscon_terminal_add_output(struct kmscon_terminal *term, struct kmscon_output *output) { @@ -263,6 +329,7 @@ void kmscon_terminal_rm_all_outputs(struct kmscon_terminal *term) void kmscon_terminal_input(struct kmscon_terminal *term, kmscon_symbol_t ch) { - kmscon_vte_input(term->vte, ch); - schedule_redraw(term); + /* FIXME: UTF-8. */ + if (ch < 128) + kmscon_pty_input(term->pty, (char *)&ch, 1); } diff --git a/src/terminal.h b/src/terminal.h index 0168b5f..70c7e12 100644 --- a/src/terminal.h +++ b/src/terminal.h @@ -41,14 +41,18 @@ 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); diff --git a/tests/test_terminal.c b/tests/test_terminal.c index 48d30ba..b30c8e1 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,6 +65,63 @@ 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("test: cannot wait on child: %m\n"); + break; + } else if (pid == 0) { + break; + } else if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) + log_info("test: child %d exited with status " + "%d\n", pid, WEXITSTATUS(status)); + else + log_debug("test: child %d exited " + "successfully\n", pid); + } else if (WIFSIGNALED(status)) { + log_debug("test: 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) { @@ -131,6 +190,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 +214,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 +251,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;