diff options
-rw-r--r-- | Makefile.am | 7 | ||||
-rw-r--r-- | configure.ac | 7 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev-evdev.c | 938 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev-internal.h | 10 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev.c | 48 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev.h | 20 |
6 files changed, 1026 insertions, 4 deletions
diff --git a/Makefile.am b/Makefile.am index 82f474e20e..b51c522443 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2968,12 +2968,14 @@ tests += \ endif libsystemd_terminal_la_CFLAGS = \ - $(AM_CFLAGS) + $(AM_CFLAGS) \ + $(TERMINAL_CFLAGS) libsystemd_terminal_la_SOURCES = \ src/libsystemd-terminal/idev.h \ src/libsystemd-terminal/idev-internal.h \ src/libsystemd-terminal/idev.c \ + src/libsystemd-terminal/idev-evdev.c \ src/libsystemd-terminal/sysview.h \ src/libsystemd-terminal/sysview-internal.h \ src/libsystemd-terminal/sysview.c \ @@ -2989,7 +2991,8 @@ libsystemd_terminal_la_SOURCES = \ libsystemd_terminal_la_LIBADD = \ libudev-internal.la \ libsystemd-internal.la \ - libsystemd-shared.la + libsystemd-shared.la \ + $(TERMINAL_LIBS) systemd_subterm_SOURCES = \ src/libsystemd-terminal/subterm.c diff --git a/configure.ac b/configure.ac index 08a8a105f8..3900c4065b 100644 --- a/configure.ac +++ b/configure.ac @@ -1066,8 +1066,11 @@ AM_CONDITIONAL(ENABLE_MULTI_SEAT_X, [test "$have_multi_seat_x" = "yes"]) have_terminal=no AC_ARG_ENABLE(terminal, AS_HELP_STRING([--enable-terminal], [enable terminal support])) if test "x$enable_terminal" = "xyes"; then - AC_DEFINE(ENABLE_TERMINAL, 1, [Define if terminal support is to be enabled]) - have_terminal=yes + PKG_CHECK_MODULES([TERMINAL], [ libevdev >= 1.2 ], [have_terminal=yes]) + AS_IF([test "x$have_terminal" != xyes -a "x$enable_terminal" = xyes], + [AC_MSG_ERROR([*** terminal support requested but required dependencies not available])], + [test "x$have_terminal" = xyes], + [AC_DEFINE(ENABLE_TERMINAL, 1, [Define if terminal support is to be enabled])]) fi AM_CONDITIONAL(ENABLE_TERMINAL, [test "x$have_terminal" = "xyes"]) diff --git a/src/libsystemd-terminal/idev-evdev.c b/src/libsystemd-terminal/idev-evdev.c new file mode 100644 index 0000000000..c93ede8dc9 --- /dev/null +++ b/src/libsystemd-terminal/idev-evdev.c @@ -0,0 +1,938 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright (C) 2014 David Herrmann <dh.herrmann@gmail.com> + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <fcntl.h> +#include <inttypes.h> +#include <libevdev/libevdev.h> +#include <libudev.h> +#include <stdbool.h> +#include <stdlib.h> +#include <systemd/sd-bus.h> +#include <systemd/sd-event.h> +#include <unistd.h> +#include "bus-util.h" +#include "hashmap.h" +#include "idev.h" +#include "idev-internal.h" +#include "macro.h" +#include "udev-util.h" +#include "util.h" + +typedef struct idev_evdev idev_evdev; +typedef struct unmanaged_evdev unmanaged_evdev; +typedef struct managed_evdev managed_evdev; + +struct idev_evdev { + struct idev_element element; + struct libevdev *evdev; + int fd; + sd_event_source *fd_src; + sd_event_source *idle_src; + + bool unsync : 1; /* not in-sync with kernel */ + bool resync : 1; /* re-syncing with kernel */ +}; + +struct unmanaged_evdev { + struct idev_evdev evdev; + char *devnode; +}; + +struct managed_evdev { + struct idev_evdev evdev; + dev_t devnum; + + sd_bus_slot *slot_pause_device; + sd_bus_slot *slot_resume_device; + sd_bus_slot *slot_take_device; + + bool requested : 1; /* TakeDevice() was sent */ + bool acquired : 1; /* TakeDevice() was successful */ +}; + +#define idev_evdev_from_element(_e) container_of((_e), idev_evdev, element) +#define unmanaged_evdev_from_element(_e) \ + container_of(idev_evdev_from_element(_e), unmanaged_evdev, evdev) +#define managed_evdev_from_element(_e) \ + container_of(idev_evdev_from_element(_e), managed_evdev, evdev) + +#define IDEV_EVDEV_INIT(_vtable, _session) ((idev_evdev){ \ + .element = IDEV_ELEMENT_INIT((_vtable), (_session)), \ + .fd = -1, \ + }) + +#define IDEV_EVDEV_NAME_MAX (8 + DECIMAL_STR_MAX(unsigned) * 2) + +static const idev_element_vtable unmanaged_evdev_vtable; +static const idev_element_vtable managed_evdev_vtable; + +static int idev_evdev_resume(idev_evdev *evdev, int dev_fd); +static void idev_evdev_pause(idev_evdev *evdev, bool release); + +/* + * Virtual Evdev Element + * The virtual evdev element is the base class of all other evdev elements. It + * uses libevdev to access the kernel evdev API. It supports asynchronous + * access revocation, re-syncing if events got dropped and more. + * This element cannot be used by itself. There must be a wrapper around it + * which opens a file-descriptor and passes it to the virtual evdev element. + */ + +static void idev_evdev_name(char *out, dev_t devnum) { + /* @out must be at least of size IDEV_EVDEV_NAME_MAX */ + sprintf(out, "evdev/%u:%u", major(devnum), minor(devnum)); +} + +static int idev_evdev_raise(idev_evdev *evdev, struct input_event *event) { + idev_data data = { + .type = IDEV_DATA_EVDEV, + .resync = evdev->resync, + .evdev = { + .event = *event, + }, + }; + + return idev_element_feed(&evdev->element, &data); +} + +static void idev_evdev_hup(idev_evdev *evdev) { + /* + * On HUP, we close the current fd via idev_evdev_pause(). This drops + * the event-sources from the main-loop and effectively puts the + * element asleep. If the HUP is part of a hotplug-event, a following + * udev-notification will destroy the element. Otherwise, the HUP is + * either result of access-revokation or a serious error. + * For unmanaged devices, we should never receive HUP (except for + * unplug-events). But if we do, something went seriously wrong and we + * shouldn't try to be clever. + * Instead, we simply stay asleep and wait for the device to be + * disabled and then re-enabled (or closed and re-opened). This will + * re-open the device node and restart the device. + * For managed devices, a HUP usually means our device-access was + * revoked. In that case, we simply put the device asleep and wait for + * logind to notify us once the device is alive again. logind also + * passes us a new fd. Hence, we don't have to re-enable the device. + * + * Long story short: The only thing we have to do here, is close() the + * file-descriptor and remove it from the main-loop. Everything else is + * handled via additional events we receive. + */ + + idev_evdev_pause(evdev, true); +} + +static int idev_evdev_io(idev_evdev *evdev) { + idev_element *e = &evdev->element; + struct input_event ev; + unsigned int flags; + int r, error = 0; + + /* + * Read input-events via libevdev until the input-queue is drained. In + * case we're disabled, don't do anything. The input-queue might + * overflow, but we don't care as we have to resync after wake-up, + * anyway. + * TODO: libevdev should give us a hint how many events to read. We + * really want to avoid starvation, so we shouldn't read forever in + * case we cannot keep up with the kernel. + * TODO: Make sure libevdev always reports SYN_DROPPED to us, regardless + * whether any event was synced afterwards. + * TODO: Forward SYN_DROPPED to attached devices. + */ + + flags = LIBEVDEV_READ_FLAG_NORMAL; + while (e->enabled) { + if (evdev->unsync) { + /* immediately resync, even if in sync right now */ + evdev->unsync = false; + evdev->resync = false; + flags = LIBEVDEV_READ_FLAG_NORMAL; + r = libevdev_next_event(evdev->evdev, flags | LIBEVDEV_READ_FLAG_FORCE_SYNC, &ev); + if (r < 0 && r != -EAGAIN) { + r = 0; + goto error; + } else if (r != LIBEVDEV_READ_STATUS_SYNC) { + log_debug("idev-evdev: %s/%s: cannot force resync: %d", + e->session->name, e->name, r); + } + } else { + r = libevdev_next_event(evdev->evdev, flags, &ev); + } + + if (evdev->resync && r == -EAGAIN) { + /* end of re-sync */ + evdev->resync = false; + flags = LIBEVDEV_READ_FLAG_NORMAL; + } else if (r == -EAGAIN) { + /* no data available */ + break; + } else if (r < 0) { + /* read error */ + goto error; + } else if (r == LIBEVDEV_READ_STATUS_SYNC) { + if (evdev->resync) { + /* sync-event */ + r = idev_evdev_raise(evdev, &ev); + if (r != 0) { + error = r; + break; + } + } else { + /* start of sync */ + evdev->resync = true; + flags = LIBEVDEV_READ_FLAG_SYNC; + } + } else { + /* normal event */ + r = idev_evdev_raise(evdev, &ev); + if (r != 0) { + error = r; + break; + } + } + } + + if (error < 0) + log_debug("idev-evdev: %s/%s: error on data event: %s", + e->session->name, e->name, strerror(-error)); + return error; + +error: + idev_evdev_hup(evdev); + return r; +} + +static int idev_evdev_event_fn(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + idev_evdev *evdev = userdata; + + /* fetch data as long as EPOLLIN is signalled */ + if (revents & EPOLLIN) + return idev_evdev_io(evdev); + + if (revents & (EPOLLHUP | EPOLLERR)) + idev_evdev_hup(evdev); + + return 0; +} + +static int idev_evdev_idle_fn(sd_event_source *s, void *userdata) { + idev_evdev *evdev = userdata; + + /* + * The idle-event is raised whenever we have to re-sync the libevdev + * state from the kernel. We simply call into idev_evdev_io() which + * flushes the state and re-syncs it if @unsync is set. + * State has to be synced whenever our view of the kernel device is + * out of date. This is the case when we open the device, if the + * kernel's receive buffer overflows, or on other exceptional + * situations. Events during re-syncs must be forwarded to the upper + * layers so they can update their view of the device. However, such + * events must only be handled passively, as they might be out-of-order + * and/or re-ordered. Therefore, we mark them as 'sync' events. + */ + + if (!evdev->unsync) + return 0; + + return idev_evdev_io(evdev); +} + +static void idev_evdev_destroy(idev_evdev *evdev) { + assert(evdev); + assert(evdev->fd < 0); + + libevdev_free(evdev->evdev); + evdev->evdev = NULL; +} + +static void idev_evdev_enable(idev_evdev *evdev) { + assert(evdev); + assert(evdev->fd_src); + assert(evdev->idle_src); + + sd_event_source_set_enabled(evdev->fd_src, SD_EVENT_ON); + sd_event_source_set_enabled(evdev->idle_src, SD_EVENT_ONESHOT); +} + +static void idev_evdev_disable(idev_evdev *evdev) { + assert(evdev); + assert(evdev->fd_src); + assert(evdev->idle_src); + + sd_event_source_set_enabled(evdev->fd_src, SD_EVENT_OFF); + sd_event_source_set_enabled(evdev->idle_src, SD_EVENT_OFF); +} + +static int idev_evdev_resume(idev_evdev *evdev, int dev_fd) { + idev_element *e = &evdev->element; + _cleanup_close_ int fd = dev_fd; + int r, flags; + + if (fd < 0 || evdev->fd == fd) { + fd = -1; + if (evdev->fd >= 0 && e->n_open > 0 && e->enabled) + idev_evdev_enable(evdev); + + return 0; + } + + idev_evdev_pause(evdev, true); + log_debug("idev-evdev: %s/%s: resume", e->session->name, e->name); + + r = fd_nonblock(fd, true); + if (r < 0) + return r; + + r = fd_cloexec(fd, true); + if (r < 0) + return r; + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return r; + + flags &= O_ACCMODE; + if (flags == O_WRONLY) + return -EACCES; + + evdev->element.readable = true; + evdev->element.writable = true; + if (flags == O_RDONLY) + evdev->element.writable = false; + else if (flags == O_WRONLY) + evdev->element.readable = false; + + /* + * TODO: We *MUST* re-sync the device so we get a delta of the changed + * state while we didn't read events from the device. This works just + * fine with libevdev_change_fd(), however, libevdev_new_from_fd() (or + * libevdev_set_fd()) don't pass us events for the initial device + * state. So even if we force a re-sync, we will not get the delta for + * the initial device state. + * We really need to fix libevdev to support that! + */ + if (evdev->evdev) + r = libevdev_change_fd(evdev->evdev, fd); + else + r = libevdev_new_from_fd(fd, &evdev->evdev); + + if (r < 0) + return r; + + r = sd_event_add_io(e->session->context->event, + &evdev->fd_src, + fd, + EPOLLHUP | EPOLLERR | EPOLLIN, + idev_evdev_event_fn, + evdev); + if (r < 0) + return r; + + r = sd_event_add_defer(e->session->context->event, + &evdev->idle_src, + idev_evdev_idle_fn, + evdev); + if (r < 0) { + evdev->fd_src = sd_event_source_unref(evdev->fd_src); + return r; + } + + if (e->n_open < 1 || !e->enabled) { + sd_event_source_set_enabled(evdev->fd_src, SD_EVENT_OFF); + sd_event_source_set_enabled(evdev->idle_src, SD_EVENT_OFF); + } + + evdev->unsync = true; + evdev->fd = fd; + + fd = -1; + return 0; +} + +static void idev_evdev_pause(idev_evdev *evdev, bool release) { + idev_element *e = &evdev->element; + + if (evdev->fd < 0) + return; + + log_debug("idev-evdev: %s/%s: pause", e->session->name, e->name); + + if (release) { + evdev->idle_src = sd_event_source_unref(evdev->idle_src); + evdev->fd_src = sd_event_source_unref(evdev->fd_src); + evdev->fd = safe_close(evdev->fd); + } else { + idev_evdev_disable(evdev); + } +} + +/* + * Unmanaged Evdev Element + * The unmanaged evdev element opens the evdev node for a given input device + * directly (/dev/input/eventX) and thus needs sufficient privileges. It opens + * the device only if we really require it and releases it as soon as we're + * disabled or closed. + * The unmanaged element can be used in all situations where you have direct + * access to input device nodes. Unlike managed evdev elements, it can be used + * outside of user sessions and in emergency situations where logind is not + * available. + */ + +static void unmanaged_evdev_resume(idev_element *e) { + unmanaged_evdev *eu = unmanaged_evdev_from_element(e); + int r, fd; + + /* + * Unmanaged devices can be acquired on-demand. Therefore, don't + * acquire it unless someone opened the device *and* we're enabled. + */ + if (e->n_open < 1 || !e->enabled) + return; + + fd = eu->evdev.fd; + if (fd < 0) { + fd = open(eu->devnode, O_RDWR | O_CLOEXEC | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { + if (errno != EACCES && errno != EPERM) { + log_debug("idev-evdev: %s/%s: cannot open node %s: %m", + e->session->name, e->name, eu->devnode); + return; + } + + fd = open(eu->devnode, O_RDONLY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { + log_debug("idev-evdev: %s/%s: cannot open node %s: %m", + e->session->name, e->name, eu->devnode); + return; + } + + e->readable = true; + e->writable = false; + } else { + e->readable = true; + e->writable = true; + } + } + + r = idev_evdev_resume(&eu->evdev, fd); + if (r < 0) + log_debug("idev-evdev: %s/%s: cannot resume: %s", + e->session->name, e->name, strerror(-r)); +} + +static void unmanaged_evdev_pause(idev_element *e) { + unmanaged_evdev *eu = unmanaged_evdev_from_element(e); + + /* + * Release the device if the device is disabled or there is no-one who + * opened it. This guarantees we stay only available if we're opened + * *and* enabled. + */ + + idev_evdev_pause(&eu->evdev, true); +} + +static int unmanaged_evdev_new(idev_element **out, idev_session *s, struct udev_device *ud) { + _cleanup_(idev_element_freep) idev_element *e = NULL; + char name[IDEV_EVDEV_NAME_MAX]; + unmanaged_evdev *eu; + const char *devnode; + dev_t devnum; + int r; + + assert_return(s, -EINVAL); + assert_return(ud, -EINVAL); + + devnode = udev_device_get_devnode(ud); + devnum = udev_device_get_devnum(ud); + if (!devnode || devnum == 0) + return -ENODEV; + + idev_evdev_name(name, devnum); + + eu = new0(unmanaged_evdev, 1); + if (!eu) + return -ENOMEM; + + e = &eu->evdev.element; + eu->evdev = IDEV_EVDEV_INIT(&unmanaged_evdev_vtable, s); + + eu->devnode = strdup(devnode); + if (!eu->devnode) + return -ENOMEM; + + r = idev_element_add(e, name); + if (r < 0) + return r; + + if (out) + *out = e; + e = NULL; + return 0; +} + +static void unmanaged_evdev_free(idev_element *e) { + unmanaged_evdev *eu = unmanaged_evdev_from_element(e); + + idev_evdev_destroy(&eu->evdev); + free(eu->devnode); + free(eu); +} + +static const idev_element_vtable unmanaged_evdev_vtable = { + .free = unmanaged_evdev_free, + .enable = unmanaged_evdev_resume, + .disable = unmanaged_evdev_pause, + .open = unmanaged_evdev_resume, + .close = unmanaged_evdev_pause, +}; + +/* + * Managed Evdev Element + * The managed evdev element uses systemd-logind to acquire evdev devices. This + * means, we do not open the device node /dev/input/eventX directly. Instead, + * logind passes us a file-descriptor whenever our session is activated. Thus, + * we don't need access to the device node directly. + * Furthermore, whenever the session is put asleep, logind revokes the + * file-descriptor so we loose access to the device. + * Managed evdev elements should be preferred over unmanaged elements whenever + * you run inside a user session with exclusive device access. + */ + +static int managed_evdev_take_device_fn(sd_bus *bus, + sd_bus_message *reply, + void *userdata, + sd_bus_error *ret_error) { + managed_evdev *em = userdata; + idev_element *e = &em->evdev.element; + idev_session *s = e->session; + int r, paused, fd; + + em->slot_take_device = sd_bus_slot_unref(em->slot_take_device); + + if (sd_bus_message_is_method_error(reply, NULL)) { + const sd_bus_error *error = sd_bus_message_get_error(reply); + + log_debug("idev-evdev: %s/%s: TakeDevice failed: %s: %s", + s->name, e->name, error->name, error->message); + return 0; + } + + em->acquired = true; + + r = sd_bus_message_read(reply, "hb", &fd, &paused); + if (r < 0) { + log_debug("idev-evdev: %s/%s: erroneous TakeDevice reply", s->name, e->name); + return 0; + } + + /* If the device is paused, ignore it; we will get the next fd via + * ResumeDevice signals. */ + if (paused) + return 0; + + fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (fd < 0) { + log_debug("idev-evdev: %s/%s: cannot duplicate evdev fd: %m", s->name, e->name); + return 0; + } + + r = idev_evdev_resume(&em->evdev, fd); + if (r < 0) + log_debug("idev-evdev: %s/%s: cannot resume: %s", + s->name, e->name, strerror(-r)); + + return 0; +} + +static void managed_evdev_resume(idev_element *e) { + _cleanup_bus_message_unref_ sd_bus_message *m = NULL; + managed_evdev *em = managed_evdev_from_element(e); + idev_session *s = e->session; + idev_context *c = s->context; + int r; + + /* + * Acquiring managed devices is heavy, so do it only once we're + * enabled *and* opened by someone. + */ + if (e->n_open < 1 || !e->enabled) + return; + + /* bail out if already pending */ + if (em->requested) + return; + + r = sd_bus_message_new_method_call(c->sysbus, + &m, + "org.freedesktop.login1", + s->path, + "org.freedesktop.login1.Session", + "TakeDevice"); + if (r < 0) + goto error; + + r = sd_bus_message_append(m, "uu", major(em->devnum), minor(em->devnum)); + if (r < 0) + goto error; + + r = sd_bus_call_async(c->sysbus, + &em->slot_take_device, + m, + managed_evdev_take_device_fn, + em, + 0); + if (r < 0) + goto error; + + em->requested = true; + return; + +error: + log_debug("idev-evdev: %s/%s: cannot send TakeDevice request: %s", + s->name, e->name, strerror(-r)); +} + +static void managed_evdev_pause(idev_element *e) { + _cleanup_bus_message_unref_ sd_bus_message *m = NULL; + managed_evdev *em = managed_evdev_from_element(e); + idev_session *s = e->session; + idev_context *c = s->context; + int r; + + /* + * Releasing managed devices is heavy. Once acquired, we get + * notifications for sleep/wake-up events, so there's no reason to + * release it if disabled but opened. However, if a device is closed, + * we release it immediately as we don't care for sleep/wake-up events + * then (even if we're actually enabled). + */ + + idev_evdev_pause(&em->evdev, false); + + if (e->n_open > 0 || !em->requested) + return; + + /* + * If TakeDevice() is pending or was successful, make sure to + * release the device again. We don't care for return-values, + * so send it without waiting or callbacks. + * If a failed TakeDevice() is pending, but someone else took + * the device on the same bus-connection, we might incorrectly + * release their device. This is an unlikely race, though. + * Furthermore, you really shouldn't have two users of the + * controller-API on the same session, on the same devices, *AND* on + * the same bus-connection. So we don't care for that race.. + */ + + idev_evdev_pause(&em->evdev, true); + em->requested = false; + + if (!em->acquired && !em->slot_take_device) + return; + + em->slot_take_device = sd_bus_slot_unref(em->slot_take_device); + em->acquired = false; + + r = sd_bus_message_new_method_call(c->sysbus, + &m, + "org.freedesktop.login1", + s->path, + "org.freedesktop.login1.Session", + "ReleaseDevice"); + if (r >= 0) { + r = sd_bus_message_append(m, "uu", major(em->devnum), minor(em->devnum)); + if (r >= 0) + r = sd_bus_send(c->sysbus, m, NULL); + } + + if (r < 0 && r != -ENOTCONN) + log_debug("idev-evdev: %s/%s: cannot send ReleaseDevice: %s", + s->name, e->name, strerror(-r)); +} + +static int managed_evdev_pause_device_fn(sd_bus *bus, + sd_bus_message *signal, + void *userdata, + sd_bus_error *ret_error) { + managed_evdev *em = userdata; + idev_element *e = &em->evdev.element; + idev_session *s = e->session; + idev_context *c = s->context; + uint32_t major, minor; + const char *mode; + int r; + + /* + * We get PauseDevice() signals from logind whenever a device we + * requested was, or is about to be, paused. Arguments are major/minor + * number of the device and the mode of the operation. + * In case the event is not about our device, we ignore it. Otherwise, + * we treat it as asynchronous access-revocation (as if we got HUP on + * the device fd). Note that we might have already treated the HUP + * event via EPOLLHUP, whichever comes first. + * + * @mode can be one of the following: + * "pause": The device is about to be paused. We must react + * immediately and respond with PauseDeviceComplete(). Once + * we replied, logind will pause the device. Note that + * logind might apply any kind of timeout and force pause + * the device if we don't respond in a timely manner. In + * this case, we will receive a second PauseDevice event + * with @mode set to "force" (or similar). + * "force": The device was disabled forecfully by logind. Access is + * already revoked. This is just an asynchronous + * notification so we can put the device asleep (in case + * we didn't already notice the access revocation). + * "gone": This is like "force" but is sent if the device was + * paused due to a device-removal event. + * + * We always handle PauseDevice signals as "force" as we properly + * support asynchronous access revocation, anyway. But in case logind + * sent mode "pause", we also call PauseDeviceComplete() to immediately + * acknowledge the request. + */ + + r = sd_bus_message_read(signal, "uus", &major, &minor, &mode); + if (r < 0) { + log_debug("idev-evdev: %s/%s: erroneous PauseDevice signal", + s->name, e->name); + return 0; + } + + /* not our device? */ + if (makedev(major, minor) != em->devnum) + return 0; + + idev_evdev_pause(&em->evdev, true); + + if (streq(mode, "pause")) { + _cleanup_bus_message_unref_ sd_bus_message *m = NULL; + + /* + * Sending PauseDeviceComplete() is racy if logind triggers the + * timeout. That is, if we take too long and logind pauses the + * device by sending a forced PauseDevice, our + * PauseDeviceComplete call will be stray. That's fine, though. + * logind ignores such stray calls. Only if logind also sent a + * further PauseDevice() signal, it might match our call + * incorrectly to the newer PauseDevice(). That's fine, too, as + * we handle that event asynchronously, anyway. Therefore, + * whatever happens, we're fine. Yay! + */ + + r = sd_bus_message_new_method_call(c->sysbus, + &m, + "org.freedesktop.login1", + s->path, + "org.freedesktop.login1.Session", + "PauseDeviceComplete"); + if (r >= 0) { + r = sd_bus_message_append(m, "uu", major, minor); + if (r >= 0) + r = sd_bus_send(c->sysbus, m, NULL); + } + + if (r < 0) + log_debug("idev-evdev: %s/%s: cannot send PauseDeviceComplete: %s", + s->name, e->name, strerror(-r)); + } + + return 0; +} + +static int managed_evdev_resume_device_fn(sd_bus *bus, + sd_bus_message *signal, + void *userdata, + sd_bus_error *ret_error) { + managed_evdev *em = userdata; + idev_element *e = &em->evdev.element; + idev_session *s = e->session; + uint32_t major, minor; + int r, fd; + + /* + * We get ResumeDevice signals whenever logind resumed a previously + * paused device. The arguments contain the major/minor number of the + * related device and a new file-descriptor for the freshly opened + * device-node. + * If the signal is not about our device, we simply ignore it. + * Otherwise, we take the file-descriptor and immediately resume the + * device. + */ + + r = sd_bus_message_read(signal, "uuh", &major, &minor, &fd); + if (r < 0) { + log_debug("idev-evdev: %s/%s: erroneous ResumeDevice signal", + s->name, e->name); + return 0; + } + + /* not our device? */ + if (makedev(major, minor) != em->devnum) + return 0; + + fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (fd < 0) { + log_debug("idev-evdev: %s/%s: cannot duplicate evdev fd: %m", + s->name, e->name); + return 0; + } + + r = idev_evdev_resume(&em->evdev, fd); + if (r < 0) + log_debug("idev-evdev: %s/%s: cannot resume: %s", + s->name, e->name, strerror(-r)); + + return 0; +} + +static int managed_evdev_setup_bus(managed_evdev *em) { + idev_element *e = &em->evdev.element; + idev_session *s = e->session; + idev_context *c = s->context; + _cleanup_free_ char *match = NULL; + int r; + + match = strjoin("type='signal'," + "sender='org.freedesktop.login1'," + "interface='org.freedesktop.login1.Session'," + "member='PauseDevice'," + "path='", s->path, "'", + NULL); + if (!match) + return -ENOMEM; + + r = sd_bus_add_match(c->sysbus, + &em->slot_pause_device, + match, + managed_evdev_pause_device_fn, + em); + if (r < 0) + return r; + + free(match); + match = strjoin("type='signal'," + "sender='org.freedesktop.login1'," + "interface='org.freedesktop.login1.Session'," + "member='ResumeDevice'," + "path='", s->path, "'", + NULL); + if (!match) + return -ENOMEM; + + r = sd_bus_add_match(c->sysbus, + &em->slot_resume_device, + match, + managed_evdev_resume_device_fn, + em); + if (r < 0) + return r; + + return 0; +} + +static int managed_evdev_new(idev_element **out, idev_session *s, struct udev_device *ud) { + _cleanup_(idev_element_freep) idev_element *e = NULL; + char name[IDEV_EVDEV_NAME_MAX]; + managed_evdev *em; + dev_t devnum; + int r; + + assert_return(s, -EINVAL); + assert_return(s->context->sysbus, -EINVAL); + assert_return(s->managed, -EINVAL); + assert_return(s->context->sysbus, -EINVAL); + assert_return(ud, -EINVAL); + + devnum = udev_device_get_devnum(ud); + if (devnum == 0) + return -ENODEV; + + idev_evdev_name(name, devnum); + + em = new0(managed_evdev, 1); + if (!em) + return -ENOMEM; + + e = &em->evdev.element; + em->evdev = IDEV_EVDEV_INIT(&managed_evdev_vtable, s); + em->devnum = devnum; + + r = managed_evdev_setup_bus(em); + if (r < 0) + return r; + + r = idev_element_add(e, name); + if (r < 0) + return r; + + if (out) + *out = e; + e = NULL; + return 0; +} + +static void managed_evdev_free(idev_element *e) { + managed_evdev *em = managed_evdev_from_element(e); + + em->slot_resume_device = sd_bus_slot_unref(em->slot_resume_device); + em->slot_pause_device = sd_bus_slot_unref(em->slot_pause_device); + idev_evdev_destroy(&em->evdev); + free(em); +} + +static const idev_element_vtable managed_evdev_vtable = { + .free = managed_evdev_free, + .enable = managed_evdev_resume, + .disable = managed_evdev_pause, + .open = managed_evdev_resume, + .close = managed_evdev_pause, +}; + +/* + * Generic Constructor + * Instead of relying on the caller to choose between managed and unmanaged + * evdev devices, the idev_evdev_new() constructor does that for you (by + * looking at s->managed). + */ + +bool idev_is_evdev(idev_element *e) { + return e && (e->vtable == &unmanaged_evdev_vtable || + e->vtable == &managed_evdev_vtable); +} + +idev_element *idev_find_evdev(idev_session *s, dev_t devnum) { + char name[IDEV_EVDEV_NAME_MAX]; + + assert_return(s, NULL); + assert_return(devnum != 0, NULL); + + idev_evdev_name(name, devnum); + return idev_find_element(s, name); +} + +int idev_evdev_new(idev_element **out, idev_session *s, struct udev_device *ud) { + assert_return(s, -EINVAL); + assert_return(ud, -EINVAL); + + return s->managed ? managed_evdev_new(out, s, ud) : unmanaged_evdev_new(out, s, ud); +} diff --git a/src/libsystemd-terminal/idev-internal.h b/src/libsystemd-terminal/idev-internal.h index bffefbb9c1..3301ebf6e4 100644 --- a/src/libsystemd-terminal/idev-internal.h +++ b/src/libsystemd-terminal/idev-internal.h @@ -22,6 +22,8 @@ #pragma once #include <inttypes.h> +#include <libudev.h> +#include <linux/input.h> #include <stdbool.h> #include <stdlib.h> #include <systemd/sd-bus.h> @@ -37,6 +39,14 @@ typedef struct idev_element idev_element; typedef struct idev_element_vtable idev_element_vtable; /* + * Evdev Elements + */ + +bool idev_is_evdev(idev_element *e); +idev_element *idev_find_evdev(idev_session *s, dev_t devnum); +int idev_evdev_new(idev_element **out, idev_session *s, struct udev_device *ud); + +/* * Element Links */ diff --git a/src/libsystemd-terminal/idev.c b/src/libsystemd-terminal/idev.c index 5e3080797a..2316a66529 100644 --- a/src/libsystemd-terminal/idev.c +++ b/src/libsystemd-terminal/idev.c @@ -20,6 +20,8 @@ ***/ #include <inttypes.h> +#include <libudev.h> +#include <linux/input.h> #include <stdbool.h> #include <stdlib.h> #include <systemd/sd-bus.h> @@ -31,6 +33,7 @@ #include "login-shared.h" #include "macro.h" #include "set.h" +#include "udev-util.h" #include "util.h" static void element_open(idev_element *e); @@ -522,6 +525,51 @@ void idev_session_disable(idev_session *s) { } } +int idev_session_add_evdev(idev_session *s, struct udev_device *ud) { + idev_element *e; + dev_t devnum; + int r; + + assert_return(s, -EINVAL); + assert_return(ud, -EINVAL); + + devnum = udev_device_get_devnum(ud); + if (devnum == 0) + return 0; + + e = idev_find_evdev(s, devnum); + if (e) + return 0; + + r = idev_evdev_new(&e, s, ud); + if (r < 0) + return r; + + r = session_add_element(s, e); + if (r != 0) + return r; + + return 0; +} + +int idev_session_remove_evdev(idev_session *s, struct udev_device *ud) { + idev_element *e; + dev_t devnum; + + assert(s); + assert(ud); + + devnum = udev_device_get_devnum(ud); + if (devnum == 0) + return 0; + + e = idev_find_evdev(s, devnum); + if (!e) + return 0; + + return session_remove_element(s, e); +} + /* * Contexts */ diff --git a/src/libsystemd-terminal/idev.h b/src/libsystemd-terminal/idev.h index 6f618f37af..c98fb1bfb0 100644 --- a/src/libsystemd-terminal/idev.h +++ b/src/libsystemd-terminal/idev.h @@ -26,6 +26,8 @@ #pragma once #include <inttypes.h> +#include <libudev.h> +#include <linux/input.h> #include <stdbool.h> #include <stdlib.h> #include <systemd/sd-bus.h> @@ -33,6 +35,7 @@ #include "util.h" typedef struct idev_data idev_data; +typedef struct idev_data_evdev idev_data_evdev; typedef struct idev_event idev_event; typedef struct idev_device idev_device; @@ -44,6 +47,7 @@ typedef struct idev_context idev_context; */ enum { + IDEV_ELEMENT_EVDEV, IDEV_ELEMENT_CNT }; @@ -52,17 +56,30 @@ enum { }; /* + * Evdev Elements + */ + +struct idev_data_evdev { + struct input_event event; +}; + +/* * Data Packets */ enum { IDEV_DATA_RESYNC, + IDEV_DATA_EVDEV, IDEV_DATA_CNT }; struct idev_data { unsigned int type; bool resync : 1; + + union { + idev_data_evdev evdev; + }; }; /* @@ -122,6 +139,9 @@ bool idev_session_is_enabled(idev_session *s); void idev_session_enable(idev_session *s); void idev_session_disable(idev_session *s); +int idev_session_add_evdev(idev_session *s, struct udev_device *ud); +int idev_session_remove_evdev(idev_session *s, struct udev_device *ud); + /* * Contexts */ |