/* * uterm - Linux User-Space Terminal * * Copyright (c) 2011-2012 David Herrmann * * 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. */ /* * Virtual Terminals */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "eloop.h" #include "log.h" #include "static_misc.h" #include "uterm.h" #define LOG_SUBSYSTEM "vt" struct uterm_vt { unsigned long ref; struct kmscon_dlist list; struct uterm_vt_master *vtm; struct uterm_input *input; unsigned int mode; uterm_vt_cb cb; void *data; bool active; /* this is for *real* linux kernel VTs */ int real_fd; int real_num; int real_saved_num; struct termios real_saved_attribs; struct ev_fd *real_efd; }; struct uterm_vt_master { unsigned long ref; struct ev_eloop *eloop; bool vt_support; struct kmscon_dlist vts; }; static int vt_call(struct uterm_vt *vt, unsigned int event) { int ret; switch (event) { case UTERM_VT_ACTIVATE: if (!vt->active) { if (vt->cb) { ret = vt->cb(vt, event, vt->data); if (ret) log_warning("vt event handler returned %d instead of 0 on activation", ret); } vt->active = true; } break; case UTERM_VT_DEACTIVATE: if (vt->active) { if (vt->cb) { ret = vt->cb(vt, event, vt->data); if (ret) return ret; } vt->active = false; } break; } return 0; } static void real_enter(struct uterm_vt *vt, struct signalfd_siginfo *info) { struct vt_stat vts; int ret; if (info->ssi_code != SI_KERNEL) return; ret = ioctl(vt->real_fd, VT_GETSTATE, &vts); if (ret || vts.v_active != vt->real_num) return; log_debug("enter VT %d %p", vt->real_num, vt); ioctl(vt->real_fd, VT_RELDISP, VT_ACKACQ); if (ioctl(vt->real_fd, KDSETMODE, KD_GRAPHICS)) log_warn("cannot set graphics mode on vt %p", vt); vt_call(vt, UTERM_VT_ACTIVATE); } static void real_leave(struct uterm_vt *vt, struct signalfd_siginfo *info) { struct vt_stat vts; int ret; if (info->ssi_code != SI_KERNEL) return; ret = ioctl(vt->real_fd, VT_GETSTATE, &vts); if (ret || vts.v_active != vt->real_num) return; if (vt_call(vt, UTERM_VT_DEACTIVATE)) { log_debug("leaving VT %d %p denied", vt->real_num, vt); ioctl(vt->real_fd, VT_RELDISP, 0); } else { log_debug("leaving VT %d %p", vt->real_num, vt); ioctl(vt->real_fd, VT_RELDISP, 1); if (ioctl(vt->real_fd, KDSETMODE, KD_TEXT)) log_warn("cannot set text mode on vt %p", vt); } } static void real_input(struct ev_fd *fd, int mask, void *data) { struct uterm_vt *vt = data; /* we ignore input from the VT because we get it from evdev */ tcflush(vt->real_fd, TCIFLUSH); } static int open_tty(int id, int *tty_fd, int *tty_num) { int fd, err1; char filename[16]; if (!tty_fd || !tty_num) return -EINVAL; if (id < 0) { fd = open("/dev/tty0", O_NONBLOCK | O_NOCTTY | O_CLOEXEC); if (fd < 0) { err1 = errno; fd = open("/dev/tty1", O_NONBLOCK | O_NOCTTY | O_CLOEXEC); if (fd < 0) { log_err("cannot find parent tty (%d, %d): %m", err1, errno); return -errno; } } if (ioctl(fd, VT_OPENQRY, &id) || id <= 0) { close(fd); log_err("cannot get unused tty"); return -EINVAL; } close(fd); } snprintf(filename, sizeof(filename), "/dev/tty%d", id); filename[sizeof(filename) - 1] = 0; log_notice("using tty %s", filename); fd = open(filename, O_RDWR | O_NOCTTY | O_CLOEXEC); if (fd < 0) { log_err("cannot open tty %s", filename); return -errno; } *tty_fd = fd; *tty_num = id; return 0; } static int real_open(struct uterm_vt *vt) { struct termios raw_attribs; struct vt_mode mode; struct vt_stat vts; int ret; sigset_t mask; log_debug("open vt %p", vt); ret = open_tty(-1, &vt->real_fd, &vt->real_num); if (ret) return ret; ret = ev_eloop_new_fd(vt->vtm->eloop, &vt->real_efd, vt->real_fd, EV_READABLE, real_input, vt); if (ret) goto err_fd; /* * Get the number of the VT which is active now, so we have something * to switch back to in kmscon_vt_switch_leave. */ ret = ioctl(vt->real_fd, VT_GETSTATE, &vts); if (ret) { log_warn("cannot find the currently active VT"); vt->real_saved_num = -1; } else { vt->real_saved_num = vts.v_active; } if (tcgetattr(vt->real_fd, &vt->real_saved_attribs) < 0) { log_err("cannot get terminal attributes"); ret = -EFAULT; goto err_eloop; } /* Ignore control characters and disable echo */ raw_attribs = vt->real_saved_attribs; cfmakeraw(&raw_attribs); /* Fix up line endings to be normal (cfmakeraw hoses them) */ raw_attribs.c_oflag |= OPOST | OCRNL; if (tcsetattr(vt->real_fd, TCSANOW, &raw_attribs) < 0) log_warn("cannot put terminal into raw mode"); if (ioctl(vt->real_fd, KDSETMODE, KD_GRAPHICS)) { log_err("vt: cannot set graphics mode\n"); ret = -errno; goto err_reset; } memset(&mode, 0, sizeof(mode)); mode.mode = VT_PROCESS; mode.acqsig = SIGUSR1; mode.relsig = SIGUSR2; if (ioctl(vt->real_fd, VT_SETMODE, &mode)) { log_err("cannot take control of vt handling"); ret = -errno; goto err_text; } sigemptyset(&mask); sigaddset(&mask, SIGUSR1); sigaddset(&mask, SIGUSR2); sigprocmask(SIG_BLOCK, &mask, NULL); return 0; err_text: ioctl(vt->real_fd, KDSETMODE, KD_TEXT); err_reset: tcsetattr(vt->real_fd, TCSANOW, &vt->real_saved_attribs); err_eloop: ev_eloop_rm_fd(vt->real_efd); vt->real_efd = NULL; err_fd: close(vt->real_fd); return ret; } static void real_close(struct uterm_vt *vt) { struct vt_mode mode; log_debug("closing vt %p", vt); memset(&mode, 0, sizeof(mode)); mode.mode = VT_AUTO; ioctl(vt->real_fd, VT_SETMODE, &mode); ioctl(vt->real_fd, KDSETMODE, KD_TEXT); tcsetattr(vt->real_fd, TCSANOW, &vt->real_saved_attribs); ev_eloop_rm_fd(vt->real_efd); vt->real_efd = NULL; close(vt->real_fd); vt->real_fd = -1; vt->real_num = -1; vt->real_saved_num = -1; } /* Switch to this VT and make it the active VT. */ static int real_activate(struct uterm_vt *vt) { int ret; if (vt->real_num < 0) return -EINVAL; ret = ioctl(vt->real_fd, VT_ACTIVATE, vt->real_num); if (ret) { log_warn("cannot enter VT %p", vt); return -EFAULT; } log_debug("entering VT %p on demand", vt); return 0; } /* * Switch back to the VT from which we started. * Note: The VT switch needs to be acknowledged by us so we need to react on * SIGUSR. This function returns -EINPROGRESS if we started the VT switch but * still needs to react on SIGUSR. Make sure you call the eloop dispatcher again * if you get -EINPROGRESS here. * * Returns 0 if we don't know the previous VT or if the previous VT is already * active. Returns -EINPROGRESS if we started the VT switch. Returns <0 on * failure. */ static int real_deactivate(struct uterm_vt *vt) { int ret; struct vt_stat vts; if (vt->real_saved_num < 0) return 0; ret = ioctl(vt->real_fd, VT_GETSTATE, &vts); if (ret) { log_warn("cannot find current VT"); return -EFAULT; } if (vts.v_active != vt->real_num) return 0; ret = ioctl(vt->real_fd, VT_ACTIVATE, vt->real_saved_num); if (ret) { log_warn("cannot leave VT %p", vt); return -EFAULT; } log_debug("leaving VT %p on demand", vt); return -EINPROGRESS; } /* * Fake VT: * For systems without CONFIG_VT or for all seats that have no real VTs (which * is all seats except seat0), we support a fake-VT mechanism. This machanism is * only used for debugging and should not be used in production. * The Fake-VT reacts on SIGUSR1 and SIGUSR2 similar to the real-vt and * activates or deactivates the VT. However, this is a global mechanism as all * fake VTs listen to the same signals. Therefore, this is not really multi-seat * safe. * TODO: Replace this with a proper multi-seat-capable fake-VT mechanism. */ static void fake_enter(struct uterm_vt *vt, struct signalfd_siginfo *info) { if (info->ssi_code != SI_USER) return; log_debug("activating fake VT due to SIGUSR1"); vt_call(vt, UTERM_VT_ACTIVATE); } static void fake_leave(struct uterm_vt *vt, struct signalfd_siginfo *info) { if (info->ssi_code != SI_USER) return; log_debug("deactivating fake VT due to SIGUSR2"); vt_call(vt, UTERM_VT_DEACTIVATE); } static bool check_vt_support(void) { if (!access("/dev/tty0", F_OK)) return true; else return false; } static void vt_input(struct uterm_input *input, struct uterm_input_event *ev, void *data) { struct uterm_vt *vt = data; if (UTERM_INPUT_HAS_MODS(ev, UTERM_MOD4_MASK | UTERM_CONTROL_MASK)) { if (ev->keysym == XKB_KEY_F12) { if (vt->active) { log_debug("deactivating fake VT due to user input"); vt_call(vt, UTERM_VT_DEACTIVATE); } else { log_debug("activating fake VT due to user input"); vt_call(vt, UTERM_VT_ACTIVATE); } } } } static void vt_idle_event(struct ev_eloop *eloop, void *unused, void *data) { struct uterm_vt *vt = data; ev_eloop_unregister_idle_cb(eloop, vt_idle_event, data); log_debug("activating fake VT on startup"); vt_call(vt, UTERM_VT_ACTIVATE); } static void vt_sigusr1(struct ev_eloop *eloop, struct signalfd_siginfo *info, void *data) { struct uterm_vt *vt = data; if (vt->mode == UTERM_VT_REAL) real_enter(vt, info); else if (vt->mode == UTERM_VT_FAKE) fake_enter(vt, info); } static void vt_sigusr2(struct ev_eloop *eloop, struct signalfd_siginfo *info, void *data) { struct uterm_vt *vt = data; if (vt->mode == UTERM_VT_REAL) real_leave(vt, info); else if (vt->mode == UTERM_VT_FAKE) fake_leave(vt, info); } int uterm_vt_allocate(struct uterm_vt_master *vtm, struct uterm_vt **out, const char *seat, struct uterm_input *input, uterm_vt_cb cb, void *data) { struct uterm_vt *vt; int ret; if (!vtm || !out) return -EINVAL; if (!seat) seat = "seat0"; vt = malloc(sizeof(*vt)); if (!vt) return -ENOMEM; memset(vt, 0, sizeof(*vt)); vt->ref = 1; vt->vtm = vtm; vt->cb = cb; vt->data = data; vt->real_fd = -1; vt->real_num = -1; vt->real_saved_num = -1; ret = ev_eloop_register_signal_cb(vtm->eloop, SIGUSR1, vt_sigusr1, vt); if (ret) goto err_free; ret = ev_eloop_register_signal_cb(vtm->eloop, SIGUSR2, vt_sigusr2, vt); if (ret) goto err_sig1; if (!strcmp(seat, "seat0") && vtm->vt_support) { vt->mode = UTERM_VT_REAL; ret = real_open(vt); if (ret) goto err_sig2; } else { vt->mode = UTERM_VT_FAKE; vt->input = input; ret = uterm_input_register_cb(vt->input, vt_input, vt); if (ret) goto err_sig2; ret = ev_eloop_register_idle_cb(vtm->eloop, vt_idle_event, vt); if (ret) goto err_input; uterm_input_ref(vt->input); uterm_input_wake_up(vt->input); } kmscon_dlist_link(&vtm->vts, &vt->list); *out = vt; return 0; err_input: uterm_input_unregister_cb(vt->input, vt_input, vt); err_sig2: ev_eloop_unregister_signal_cb(vtm->eloop, SIGUSR2, vt_sigusr2, vt); err_sig1: ev_eloop_unregister_signal_cb(vtm->eloop, SIGUSR1, vt_sigusr1, vt); err_free: free(vt); return ret; } void uterm_vt_deallocate(struct uterm_vt *vt) { unsigned int mode; if (!vt || !vt->vtm || vt->mode == UTERM_VT_DEAD) return; mode = vt->mode; vt->mode = UTERM_VT_DEAD; if (mode == UTERM_VT_REAL) { real_close(vt); } else if (mode == UTERM_VT_FAKE) { ev_eloop_unregister_idle_cb(vt->vtm->eloop, vt_idle_event, vt); vt_call(vt, UTERM_VT_DEACTIVATE); } ev_eloop_unregister_signal_cb(vt->vtm->eloop, SIGUSR2, vt_sigusr2, vt); ev_eloop_unregister_signal_cb(vt->vtm->eloop, SIGUSR1, vt_sigusr1, vt); kmscon_dlist_unlink(&vt->list); uterm_input_sleep(vt->input); uterm_input_unref(vt->input); vt->vtm = NULL; uterm_vt_unref(vt); } void uterm_vt_ref(struct uterm_vt *vt) { if (!vt || !vt->ref) return; ++vt->ref; } void uterm_vt_unref(struct uterm_vt *vt) { if (!vt || !vt->ref || --vt->ref) return; uterm_vt_deallocate(vt); free(vt); } int uterm_vt_activate(struct uterm_vt *vt) { if (!vt || !vt->vtm) return -EINVAL; if (vt->mode == UTERM_VT_REAL) return real_activate(vt); else return -EFAULT; } int uterm_vt_deactivate(struct uterm_vt *vt) { if (!vt || !vt->vtm) return -EINVAL; if (vt->mode == UTERM_VT_REAL) return real_deactivate(vt); else return -EFAULT; } int uterm_vt_master_new(struct uterm_vt_master **out, struct ev_eloop *eloop) { struct uterm_vt_master *vtm; if (!out || !eloop) return -EINVAL; vtm = malloc(sizeof(*vtm)); if (!vtm) return -ENOMEM; memset(vtm, 0, sizeof(*vtm)); vtm->ref = 1; vtm->eloop = eloop; kmscon_dlist_init(&vtm->vts); vtm->vt_support = check_vt_support(); ev_eloop_ref(vtm->eloop); *out = vtm; return 0; } void uterm_vt_master_ref(struct uterm_vt_master *vtm) { if (!vtm || !vtm->ref) return; ++vtm->ref; } void uterm_vt_master_unref(struct uterm_vt_master *vtm) { struct uterm_vt *vt; if (!vtm || !vtm->ref || --vtm->ref) return; while (vtm->vts.next != &vtm->vts) { vt = kmscon_dlist_entry(vtm->vts.next, struct uterm_vt, list); uterm_vt_deallocate(vt); } ev_eloop_unref(vtm->eloop); free(vtm); }