From 27ed584eba41f6a764e77ada7353c66b766bce41 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Tue, 4 Feb 2025 14:41:43 +0100 Subject: [PATCH 1/5] Add basic test for Power Button handling This checks that power button is handled correctly by the custom handling, in spite of systemd handling being suppressed. --- testing/integration/power-button-shutdown.nix | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 testing/integration/power-button-shutdown.nix diff --git a/testing/integration/power-button-shutdown.nix b/testing/integration/power-button-shutdown.nix new file mode 100644 index 00000000..fb87a758 --- /dev/null +++ b/testing/integration/power-button-shutdown.nix @@ -0,0 +1,28 @@ +let + pkgs = import ../../pkgs { }; +in +pkgs.testers.runNixOSTest { + name = "ACPI power button handling"; + + nodes = { + machine = { config, pkgs, ... }: { + imports = [ ../../application/power-management ]; + }; + }; + + testScript = {nodes}: +'' +machine.start() +machine.wait_for_unit("multi-user.target") +machine.wait_for_console_text("Power Button") + +print(machine.succeed("cat /proc/bus/input/devices")) + +# Trigger ACPI shutdown command and expect shutdown +machine.send_monitor_command("system_powerdown") +machine.wait_for_console_text("Stopped target Multi-User System") + +# Test script does not finish on its own after shutdown +exit(0) +''; +} From 309a73686a34e15b44eceddca8d77d9badae651c Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Tue, 4 Feb 2025 14:41:29 +0100 Subject: [PATCH 2/5] Add support for multiple 'Power Button' devices The original script assumed that there is only one device named 'Power Button', and therefore listened to events from the first such input device. This could let power button presses go unhandled on machines with several such buttons (whether virtual or real). This change makes sure that each appropriately named device is handled, checking for the power key in addition to the name before installing a handler. --- application/power-management/default.nix | 2 +- .../power-management/power-button-shutdown.py | 62 +++++++++++++++---- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/application/power-management/default.nix b/application/power-management/default.nix index c74c5bde..4b9c3bb4 100644 --- a/application/power-management/default.nix +++ b/application/power-management/default.nix @@ -60,7 +60,7 @@ in { systemd.services.power-button-shutdown = { enable = true; - description = "Detect Power Button key presses, by Power Button device only and not remote controls, then shutdown computer"; + description = "Handle Power Button device key presses."; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${power-button-shutdown}/bin/power-button-shutdown"; diff --git a/application/power-management/power-button-shutdown.py b/application/power-management/power-button-shutdown.py index 60311cb5..8fda6e43 100644 --- a/application/power-management/power-button-shutdown.py +++ b/application/power-management/power-button-shutdown.py @@ -2,22 +2,58 @@ import evdev import logging -import os +import subprocess import sys +import threading logging.basicConfig(level=logging.INFO) -devices = [evdev.InputDevice(path) for path in evdev.list_devices()] -device_names = ', '.join([d.name for d in devices]) -logging.info(f'Found devices: {device_names}') +def handle_device(device): + """Handle power key presses for the given input device by invoking poweroff.""" -power_off_device = next((d for d in devices if d.name == 'Power Button'), None) -if power_off_device is None: - logging.error(f'Power Button device not found') - sys.exit(1) + logging.info(f'Listening to Power Button on device {device.path}') + try: + for event in device.read_loop(): + if event.type == evdev.ecodes.EV_KEY and event.code == evdev.ecodes.KEY_POWER and event.value: + logging.info(f'KEY_POWER detected on {device.path}, shutting down') + subprocess.run(['systemctl', 'start', 'poweroff.target'], check=True) + except Exception as e: + logging.error(f'Error handling {device.path}: {e}') + +def get_power_button_devices(): + """Get all input devices which identify as 'Power Button' and have a power key.""" + + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + + power_button_devices = [] + for device in devices: + try: + capabilities = device.capabilities() + if device.name == 'Power Button' and evdev.ecodes.EV_KEY in capabilities and evdev.ecodes.KEY_POWER in capabilities[evdev.ecodes.EV_KEY]: + power_button_devices.append(device) + except Exception as e: + logging.warning(f'Failed to inspect {device.path}: {e}') + + return power_button_devices + +# Identify Power Button devices +power_button_devices = get_power_button_devices() +if not power_button_devices: + logging.error('No Power Button devices found') + sys.exit(1) +logging.info(f'Found {len(power_button_devices)} Power Button device(s)') + +# Start a thread with a handler for each identified device +handler_threads = [] +for device in power_button_devices: + thread = threading.Thread(target=handle_device, args=(device,)) + thread.start() + handler_threads.append(thread) + +# Exit gracefully if a handler executes +try: + for thread in handler_threads: + thread.join() +except KeyboardInterrupt: + sys.exit(0) -logging.info(f'Listening to Power Button on device {power_off_device.path}') -for event in power_off_device.read_loop(): - if event.type == evdev.ecodes.EV_KEY and evdev.ecodes.KEY[event.code] == 'KEY_POWER' and event.value: - logging.info('KEY_POWER detected on Power Button device, shutting down') - os.system('systemctl start poweroff.target') From 30be6b9592616046c9cbf4cacb46425488d8b9bf Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Tue, 4 Feb 2025 14:49:30 +0100 Subject: [PATCH 3/5] Update changelog for multiple power buttons --- controller/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/Changelog.md b/controller/Changelog.md index 3f595dfb..a4f98b76 100644 --- a/controller/Changelog.md +++ b/controller/Changelog.md @@ -17,6 +17,7 @@ - controller: Suppress password prompt for open WiFi networks - controller: Explicitly mark WiFi networks with unsupported authentication methods - controller: Improve error messages when connecting to WiFi networks fails +- os: Extend Power Button handling to multiple devices ## Removed From d3799ad9ade07f99df9f67e71c22c9ebc0750d24 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Tue, 4 Feb 2025 15:39:51 +0100 Subject: [PATCH 4/5] Extend power button test to regular keyboard being ignored --- testing/integration/power-button-shutdown.nix | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/testing/integration/power-button-shutdown.nix b/testing/integration/power-button-shutdown.nix index fb87a758..43a9fc93 100644 --- a/testing/integration/power-button-shutdown.nix +++ b/testing/integration/power-button-shutdown.nix @@ -10,17 +10,33 @@ pkgs.testers.runNixOSTest { }; }; + extraPythonPackages = ps: [ + ps.colorama + ps.types-colorama + ]; + testScript = {nodes}: '' -machine.start() -machine.wait_for_unit("multi-user.target") -machine.wait_for_console_text("Power Button") +${builtins.readFile ../helpers/nixos-test-script-helpers.py} +import time + +with TestPrecondition("Power Button has been recognized"): + machine.start() + machine.wait_for_unit("multi-user.target") + machine.wait_for_console_text("Power Button") + + print(machine.succeed("cat /proc/bus/input/devices")) -print(machine.succeed("cat /proc/bus/input/devices")) +with TestCase("Short press on power and sleep from regular keyboard are ignored"): + # https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us + machine.send_monitor_command("sendkey 0xde") # XF86PowerOff + machine.send_monitor_command("sendkey 0xdf") # XF86Sleep + time.sleep(5) + machine.succeed("echo still alive", timeout=1) -# Trigger ACPI shutdown command and expect shutdown -machine.send_monitor_command("system_powerdown") -machine.wait_for_console_text("Stopped target Multi-User System") +with TestCase("ACPI shutdown command invokes shutdown"): + machine.send_monitor_command("system_powerdown") + machine.wait_for_console_text("Stopped target Multi-User System") # Test script does not finish on its own after shutdown exit(0) From fb777c6092d87603bed51a7cd49961c1432bd1bc Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Mon, 10 Feb 2025 13:30:10 +0100 Subject: [PATCH 5/5] Rewrite guard for legibility --- application/power-management/power-button-shutdown.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/power-management/power-button-shutdown.py b/application/power-management/power-button-shutdown.py index 8fda6e43..49e292cd 100644 --- a/application/power-management/power-button-shutdown.py +++ b/application/power-management/power-button-shutdown.py @@ -28,9 +28,10 @@ def get_power_button_devices(): power_button_devices = [] for device in devices: try: - capabilities = device.capabilities() - if device.name == 'Power Button' and evdev.ecodes.EV_KEY in capabilities and evdev.ecodes.KEY_POWER in capabilities[evdev.ecodes.EV_KEY]: - power_button_devices.append(device) + if device.name == 'Power Button': + ev_capabilities = device.capabilities().get(evdev.ecodes.EV_KEY, []) + if evdev.ecodes.KEY_POWER in ev_capabilities: + power_button_devices.append(device) except Exception as e: logging.warning(f'Failed to inspect {device.path}: {e}')