diff options
Diffstat (limited to 'src/login/logind-button.c')
-rw-r--r-- | src/login/logind-button.c | 521 |
1 files changed, 521 insertions, 0 deletions
diff --git a/src/login/logind-button.c b/src/login/logind-button.c new file mode 100644 index 0000000..0314ae1 --- /dev/null +++ b/src/login/logind-button.c @@ -0,0 +1,521 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <unistd.h> + +#include "sd-messages.h" + +#include "alloc-util.h" +#include "async.h" +#include "fd-util.h" +#include "logind-button.h" +#include "missing_input.h" +#include "string-util.h" +#include "util.h" + +#define CONST_MAX5(a, b, c, d, e) CONST_MAX(CONST_MAX(a, b), CONST_MAX(CONST_MAX(c, d), e)) + +#define ULONG_BITS (sizeof(unsigned long)*8) + +#define LONG_PRESS_DURATION (5 * USEC_PER_SEC) + +static bool bitset_get(const unsigned long *bits, unsigned i) { + return (bits[i / ULONG_BITS] >> (i % ULONG_BITS)) & 1UL; +} + +static void bitset_put(unsigned long *bits, unsigned i) { + bits[i / ULONG_BITS] |= (unsigned long) 1 << (i % ULONG_BITS); +} + +Button* button_new(Manager *m, const char *name) { + Button *b; + + assert(m); + assert(name); + + b = new0(Button, 1); + if (!b) + return NULL; + + b->name = strdup(name); + if (!b->name) + return mfree(b); + + if (hashmap_put(m->buttons, b->name, b) < 0) { + free(b->name); + return mfree(b); + } + + b->manager = m; + b->fd = -1; + + return b; +} + +void button_free(Button *b) { + assert(b); + + hashmap_remove(b->manager->buttons, b->name); + + sd_event_source_unref(b->io_event_source); + sd_event_source_unref(b->check_event_source); + + asynchronous_close(b->fd); + + free(b->name); + free(b->seat); + free(b); +} + +int button_set_seat(Button *b, const char *sn) { + assert(b); + + return free_and_strdup(&b->seat, sn); +} + +static void button_lid_switch_handle_action(Manager *manager, bool is_edge) { + HandleAction handle_action; + + assert(manager); + + /* If we are docked or on external power, handle the lid switch + * differently */ + if (manager_is_docked_or_external_displays(manager)) + handle_action = manager->handle_lid_switch_docked; + else if (handle_action_valid(manager->handle_lid_switch_ep) && manager_is_on_external_power()) + handle_action = manager->handle_lid_switch_ep; + else + handle_action = manager->handle_lid_switch; + + manager_handle_action(manager, INHIBIT_HANDLE_LID_SWITCH, handle_action, manager->lid_switch_ignore_inhibited, is_edge); +} + +static int button_recheck(sd_event_source *e, void *userdata) { + Button *b = ASSERT_PTR(userdata); + + assert(b->lid_closed); + + button_lid_switch_handle_action(b->manager, false); + return 1; +} + +static int button_install_check_event_source(Button *b) { + int r; + assert(b); + + /* Install a post handler, so that we keep rechecking as long as the lid is closed. */ + + if (b->check_event_source) + return 0; + + r = sd_event_add_post(b->manager->event, &b->check_event_source, button_recheck, b); + if (r < 0) + return r; + + return sd_event_source_set_priority(b->check_event_source, SD_EVENT_PRIORITY_IDLE+1); +} + +static int long_press_of_power_key_handler(sd_event_source *e, uint64_t usec, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(e); + + m->power_key_long_press_event_source = sd_event_source_unref(m->power_key_long_press_event_source); + + log_struct(LOG_INFO, + LOG_MESSAGE("Power key pressed long."), + "MESSAGE_ID=" SD_MESSAGE_POWER_KEY_LONG_PRESS_STR); + + manager_handle_action(m, INHIBIT_HANDLE_POWER_KEY, m->handle_power_key_long_press, m->power_key_ignore_inhibited, true); + return 0; +} + +static int long_press_of_reboot_key_handler(sd_event_source *e, uint64_t usec, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(e); + + m->reboot_key_long_press_event_source = sd_event_source_unref(m->reboot_key_long_press_event_source); + + log_struct(LOG_INFO, + LOG_MESSAGE("Reboot key pressed long."), + "MESSAGE_ID=" SD_MESSAGE_REBOOT_KEY_LONG_PRESS_STR); + + manager_handle_action(m, INHIBIT_HANDLE_REBOOT_KEY, m->handle_reboot_key_long_press, m->reboot_key_ignore_inhibited, true); + return 0; +} + +static int long_press_of_suspend_key_handler(sd_event_source *e, uint64_t usec, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(e); + + m->suspend_key_long_press_event_source = sd_event_source_unref(m->suspend_key_long_press_event_source); + + log_struct(LOG_INFO, + LOG_MESSAGE("Suspend key pressed long."), + "MESSAGE_ID=" SD_MESSAGE_SUSPEND_KEY_LONG_PRESS_STR); + + manager_handle_action(m, INHIBIT_HANDLE_SUSPEND_KEY, m->handle_suspend_key_long_press, m->suspend_key_ignore_inhibited, true); + return 0; +} + +static int long_press_of_hibernate_key_handler(sd_event_source *e, uint64_t usec, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(e); + + m->hibernate_key_long_press_event_source = sd_event_source_unref(m->hibernate_key_long_press_event_source); + + log_struct(LOG_INFO, + LOG_MESSAGE("Hibernate key pressed long."), + "MESSAGE_ID=" SD_MESSAGE_HIBERNATE_KEY_LONG_PRESS_STR); + + manager_handle_action(m, INHIBIT_HANDLE_HIBERNATE_KEY, m->handle_hibernate_key_long_press, m->hibernate_key_ignore_inhibited, true); + return 0; +} + +static void start_long_press(Manager *m, sd_event_source **e, sd_event_time_handler_t callback) { + int r; + + assert(m); + assert(e); + + if (*e) + return; + + r = sd_event_add_time_relative( + m->event, + e, + CLOCK_MONOTONIC, + LONG_PRESS_DURATION, 0, + callback, m); + if (r < 0) + log_warning_errno(r, "Failed to add long press timer event, ignoring: %m"); +} + +static int button_dispatch(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Button *b = ASSERT_PTR(userdata); + struct input_event ev; + ssize_t l; + + assert(s); + assert(fd == b->fd); + + l = read(b->fd, &ev, sizeof(ev)); + if (l < 0) + return errno != EAGAIN ? -errno : 0; + if ((size_t) l < sizeof(ev)) + return -EIO; + + if (ev.type == EV_KEY && ev.value > 0) { + + switch (ev.code) { + + case KEY_POWER: + case KEY_POWER2: + if (b->manager->handle_power_key_long_press != HANDLE_IGNORE && b->manager->handle_power_key_long_press != b->manager->handle_power_key) { + log_debug("Power key pressed. Further action depends on the key press duration."); + start_long_press(b->manager, &b->manager->power_key_long_press_event_source, long_press_of_power_key_handler); + } else { + log_struct(LOG_INFO, + LOG_MESSAGE("Power key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_POWER_KEY_STR); + manager_handle_action(b->manager, INHIBIT_HANDLE_POWER_KEY, b->manager->handle_power_key, b->manager->power_key_ignore_inhibited, true); + } + break; + + /* The kernel naming is a bit confusing here: + KEY_RESTART was probably introduced for media playback purposes, but + is now being predominantly used to indicate device reboot. + */ + + case KEY_RESTART: + if (b->manager->handle_reboot_key_long_press != HANDLE_IGNORE && b->manager->handle_reboot_key_long_press != b->manager->handle_reboot_key) { + log_debug("Reboot key pressed. Further action depends on the key press duration."); + start_long_press(b->manager, &b->manager->reboot_key_long_press_event_source, long_press_of_reboot_key_handler); + } else { + log_struct(LOG_INFO, + LOG_MESSAGE("Reboot key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_REBOOT_KEY_STR); + manager_handle_action(b->manager, INHIBIT_HANDLE_REBOOT_KEY, b->manager->handle_reboot_key, b->manager->reboot_key_ignore_inhibited, true); + } + break; + + /* The kernel naming is a bit confusing here: + + KEY_SLEEP = suspend-to-ram, which everybody else calls "suspend" + KEY_SUSPEND = suspend-to-disk, which everybody else calls "hibernate" + */ + + case KEY_SLEEP: + if (b->manager->handle_suspend_key_long_press != HANDLE_IGNORE && b->manager->handle_suspend_key_long_press != b->manager->handle_suspend_key) { + log_debug("Suspend key pressed. Further action depends on the key press duration."); + start_long_press(b->manager, &b->manager->suspend_key_long_press_event_source, long_press_of_suspend_key_handler); + } else { + log_struct(LOG_INFO, + LOG_MESSAGE("Suspend key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_SUSPEND_KEY_STR); + manager_handle_action(b->manager, INHIBIT_HANDLE_SUSPEND_KEY, b->manager->handle_suspend_key, b->manager->suspend_key_ignore_inhibited, true); + } + break; + + case KEY_SUSPEND: + if (b->manager->handle_hibernate_key_long_press != HANDLE_IGNORE && b->manager->handle_hibernate_key_long_press != b->manager->handle_hibernate_key) { + log_debug("Hibernate key pressed. Further action depends on the key press duration."); + start_long_press(b->manager, &b->manager->hibernate_key_long_press_event_source, long_press_of_hibernate_key_handler); + } else { + log_struct(LOG_INFO, + LOG_MESSAGE("Hibernate key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_HIBERNATE_KEY_STR); + manager_handle_action(b->manager, INHIBIT_HANDLE_HIBERNATE_KEY, b->manager->handle_hibernate_key, b->manager->hibernate_key_ignore_inhibited, true); + } + break; + } + + } else if (ev.type == EV_KEY && ev.value == 0) { + + switch (ev.code) { + + case KEY_POWER: + case KEY_POWER2: + if (b->manager->power_key_long_press_event_source) { + /* Long press event timer is still pending and key release + event happened. This means that key press duration was + insufficient to trigger a long press event + */ + log_struct(LOG_INFO, + LOG_MESSAGE("Power key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_POWER_KEY_STR); + + b->manager->power_key_long_press_event_source = sd_event_source_unref(b->manager->power_key_long_press_event_source); + + manager_handle_action(b->manager, INHIBIT_HANDLE_POWER_KEY, b->manager->handle_power_key, b->manager->power_key_ignore_inhibited, true); + } + break; + + case KEY_RESTART: + if (b->manager->reboot_key_long_press_event_source) { + log_struct(LOG_INFO, + LOG_MESSAGE("Reboot key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_REBOOT_KEY_STR); + + b->manager->reboot_key_long_press_event_source = sd_event_source_unref(b->manager->reboot_key_long_press_event_source); + + manager_handle_action(b->manager, INHIBIT_HANDLE_REBOOT_KEY, b->manager->handle_reboot_key, b->manager->reboot_key_ignore_inhibited, true); + } + break; + + case KEY_SLEEP: + if (b->manager->suspend_key_long_press_event_source) { + log_struct(LOG_INFO, + LOG_MESSAGE("Suspend key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_SUSPEND_KEY_STR); + + b->manager->suspend_key_long_press_event_source = sd_event_source_unref(b->manager->suspend_key_long_press_event_source); + + manager_handle_action(b->manager, INHIBIT_HANDLE_SUSPEND_KEY, b->manager->handle_suspend_key, b->manager->suspend_key_ignore_inhibited, true); + } + break; + case KEY_SUSPEND: + if (b->manager->hibernate_key_long_press_event_source) { + log_struct(LOG_INFO, + LOG_MESSAGE("Hibernate key pressed short."), + "MESSAGE_ID=" SD_MESSAGE_HIBERNATE_KEY_STR); + + b->manager->hibernate_key_long_press_event_source = sd_event_source_unref(b->manager->hibernate_key_long_press_event_source); + + manager_handle_action(b->manager, INHIBIT_HANDLE_HIBERNATE_KEY, b->manager->handle_hibernate_key, b->manager->hibernate_key_ignore_inhibited, true); + } + break; + } + + } else if (ev.type == EV_SW && ev.value > 0) { + + if (ev.code == SW_LID) { + log_struct(LOG_INFO, + LOG_MESSAGE("Lid closed."), + "MESSAGE_ID=" SD_MESSAGE_LID_CLOSED_STR); + + b->lid_closed = true; + button_lid_switch_handle_action(b->manager, true); + button_install_check_event_source(b); + + } else if (ev.code == SW_DOCK) { + log_struct(LOG_INFO, + LOG_MESSAGE("System docked."), + "MESSAGE_ID=" SD_MESSAGE_SYSTEM_DOCKED_STR); + + b->docked = true; + } + + } else if (ev.type == EV_SW && ev.value == 0) { + + if (ev.code == SW_LID) { + log_struct(LOG_INFO, + LOG_MESSAGE("Lid opened."), + "MESSAGE_ID=" SD_MESSAGE_LID_OPENED_STR); + + b->lid_closed = false; + b->check_event_source = sd_event_source_unref(b->check_event_source); + + } else if (ev.code == SW_DOCK) { + log_struct(LOG_INFO, + LOG_MESSAGE("System undocked."), + "MESSAGE_ID=" SD_MESSAGE_SYSTEM_UNDOCKED_STR); + + b->docked = false; + } + } + + return 0; +} + +static int button_suitable(int fd) { + unsigned long types[CONST_MAX(EV_KEY, EV_SW)/ULONG_BITS+1]; + + assert(fd >= 0); + + if (ioctl(fd, EVIOCGBIT(EV_SYN, sizeof types), types) < 0) + return -errno; + + if (bitset_get(types, EV_KEY)) { + unsigned long keys[CONST_MAX5(KEY_POWER, KEY_POWER2, KEY_SLEEP, KEY_SUSPEND, KEY_RESTART)/ULONG_BITS+1]; + + if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof keys), keys) < 0) + return -errno; + + if (bitset_get(keys, KEY_POWER) || + bitset_get(keys, KEY_POWER2) || + bitset_get(keys, KEY_SLEEP) || + bitset_get(keys, KEY_SUSPEND) || + bitset_get(keys, KEY_RESTART)) + return true; + } + + if (bitset_get(types, EV_SW)) { + unsigned long switches[CONST_MAX(SW_LID, SW_DOCK)/ULONG_BITS+1]; + + if (ioctl(fd, EVIOCGBIT(EV_SW, sizeof switches), switches) < 0) + return -errno; + + if (bitset_get(switches, SW_LID) || + bitset_get(switches, SW_DOCK)) + return true; + } + + return false; +} + +static int button_set_mask(const char *name, int fd) { + unsigned long + types[CONST_MAX(EV_KEY, EV_SW)/ULONG_BITS+1] = {}, + keys[CONST_MAX5(KEY_POWER, KEY_POWER2, KEY_SLEEP, KEY_SUSPEND, KEY_RESTART)/ULONG_BITS+1] = {}, + switches[CONST_MAX(SW_LID, SW_DOCK)/ULONG_BITS+1] = {}; + struct input_mask mask; + + assert(name); + assert(fd >= 0); + + bitset_put(types, EV_KEY); + bitset_put(types, EV_SW); + + mask = (struct input_mask) { + .type = EV_SYN, + .codes_size = sizeof(types), + .codes_ptr = PTR_TO_UINT64(types), + }; + + if (ioctl(fd, EVIOCSMASK, &mask) < 0) + /* Log only at debug level if the kernel doesn't do EVIOCSMASK yet */ + return log_full_errno(IN_SET(errno, ENOTTY, EOPNOTSUPP, EINVAL) ? LOG_DEBUG : LOG_WARNING, + errno, "Failed to set EV_SYN event mask on /dev/input/%s: %m", name); + + bitset_put(keys, KEY_POWER); + bitset_put(keys, KEY_POWER2); + bitset_put(keys, KEY_SLEEP); + bitset_put(keys, KEY_SUSPEND); + bitset_put(keys, KEY_RESTART); + + mask = (struct input_mask) { + .type = EV_KEY, + .codes_size = sizeof(keys), + .codes_ptr = PTR_TO_UINT64(keys), + }; + + if (ioctl(fd, EVIOCSMASK, &mask) < 0) + return log_warning_errno(errno, "Failed to set EV_KEY event mask on /dev/input/%s: %m", name); + + bitset_put(switches, SW_LID); + bitset_put(switches, SW_DOCK); + + mask = (struct input_mask) { + .type = EV_SW, + .codes_size = sizeof(switches), + .codes_ptr = PTR_TO_UINT64(switches), + }; + + if (ioctl(fd, EVIOCSMASK, &mask) < 0) + return log_warning_errno(errno, "Failed to set EV_SW event mask on /dev/input/%s: %m", name); + + return 0; +} + +int button_open(Button *b) { + _cleanup_(asynchronous_closep) int fd = -1; + const char *p; + char name[256]; + int r; + + assert(b); + + b->fd = asynchronous_close(b->fd); + + p = strjoina("/dev/input/", b->name); + + fd = open(p, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return log_warning_errno(errno, "Failed to open %s: %m", p); + + r = button_suitable(fd); + if (r < 0) + return log_warning_errno(r, "Failed to determine whether input device %s is relevant to us: %m", p); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "Device %s does not expose keys or switches relevant to us, ignoring.", p); + + if (ioctl(fd, EVIOCGNAME(sizeof name), name) < 0) + return log_error_errno(errno, "Failed to get input name for %s: %m", p); + + (void) button_set_mask(b->name, fd); + + b->io_event_source = sd_event_source_unref(b->io_event_source); + r = sd_event_add_io(b->manager->event, &b->io_event_source, fd, EPOLLIN, button_dispatch, b); + if (r < 0) + return log_error_errno(r, "Failed to add button event for %s: %m", p); + + b->fd = TAKE_FD(fd); + log_info("Watching system buttons on %s (%s)", p, name); + return 0; +} + +int button_check_switches(Button *b) { + unsigned long switches[CONST_MAX(SW_LID, SW_DOCK)/ULONG_BITS+1] = {}; + assert(b); + + if (b->fd < 0) + return -EINVAL; + + if (ioctl(b->fd, EVIOCGSW(sizeof(switches)), switches) < 0) + return -errno; + + b->lid_closed = bitset_get(switches, SW_LID); + b->docked = bitset_get(switches, SW_DOCK); + + if (b->lid_closed) + button_install_check_event_source(b); + + return 0; +} |