fix(snap): add hardware-observe so hotkeys can read input devices (#858)
Snap confinement denies /proc/bus/input/devices unless hardware-observe is connected. Declare the plug, tell users to connect it next to raw-input, and update the in-app hints. Fixes #857
This commit is contained in:
@@ -181,6 +181,7 @@ Listing: [snapcraft.io/vocalinux](https://snapcraft.io/vocalinux). `stable` is s
|
||||
sudo snap install vocalinux --edge
|
||||
sudo snap connect vocalinux:audio-record # if mic is not auto-connected
|
||||
sudo snap connect vocalinux:raw-input # global keyboard shortcuts (evdev)
|
||||
sudo snap connect vocalinux:hardware-observe # list keyboards (/proc/bus/input/devices)
|
||||
sudo snap connect vocalinux:uinput # native Wayland typing (ydotool)
|
||||
```
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ Store install, once Canonical lists the revision:
|
||||
sudo snap install vocalinux --edge
|
||||
sudo snap connect vocalinux:audio-record # if mic is not auto-connected
|
||||
sudo snap connect vocalinux:raw-input # global keyboard shortcuts (evdev)
|
||||
sudo snap connect vocalinux:hardware-observe # list keyboards (/proc/bus/input/devices)
|
||||
sudo snap connect vocalinux:uinput # native Wayland typing (ydotool)
|
||||
```
|
||||
|
||||
@@ -141,6 +142,7 @@ Sideload the GitHub `.snap` (amd64) while the Store is waiting:
|
||||
sudo snap install --dangerous ./vocalinux_0.17.0_amd64.snap
|
||||
sudo snap connect vocalinux:audio-record
|
||||
sudo snap connect vocalinux:raw-input
|
||||
sudo snap connect vocalinux:hardware-observe
|
||||
sudo snap connect vocalinux:uinput
|
||||
```
|
||||
|
||||
|
||||
+5
-1
@@ -30,6 +30,7 @@ description: |
|
||||
|
||||
Sandbox notes:
|
||||
- Global hotkeys need: sudo snap connect vocalinux:raw-input
|
||||
and sudo snap connect vocalinux:hardware-observe
|
||||
- Native Wayland typing needs: sudo snap connect vocalinux:uinput
|
||||
(v0.17.0 ships the plug; v0.16.2 edge (rev 7) has no uinput plug; refresh first)
|
||||
- Microphone may need: sudo snap connect vocalinux:audio-record
|
||||
@@ -57,10 +58,13 @@ apps:
|
||||
- home # optional access outside snap HOME (shared files)
|
||||
- network # download speech models on first run
|
||||
- network-bind # optional remote_api engine
|
||||
# Global hotkeys via python-evdev (/proc/bus/input + /dev/input/event*).
|
||||
# Global hotkeys via python-evdev (/dev/input/event*).
|
||||
# Super-privileged: not auto-connected; user must:
|
||||
# sudo snap connect vocalinux:raw-input
|
||||
- raw-input
|
||||
# Needed for /proc/bus/input/devices; not auto-connected:
|
||||
# sudo snap connect vocalinux:hardware-observe
|
||||
- hardware-observe
|
||||
# ydotool writes to /dev/uinput so dictation reaches native Wayland apps.
|
||||
# Super-privileged: not auto-connected; user must:
|
||||
# sudo snap connect vocalinux:uinput
|
||||
|
||||
@@ -127,8 +127,8 @@ def find_keyboard_devices() -> list[str]:
|
||||
|
||||
Prefers /proc/bus/input/devices (host installs). Falls back to
|
||||
``evdev.list_devices()`` when that path cannot be read (common under snap
|
||||
confinement even with raw-input, which grants /dev/input/event* but may
|
||||
still deny /proc/bus/input/devices).
|
||||
confinement: raw-input grants /dev/input/event*, while hardware-observe
|
||||
is the plug that grants /proc/bus/input/devices).
|
||||
|
||||
Returns:
|
||||
List of device paths for keyboard devices
|
||||
@@ -424,7 +424,8 @@ class EvdevKeyboardBackend(KeyboardBackend):
|
||||
return (
|
||||
"Snap is missing input-device access. "
|
||||
"Connect once, then restart:\n"
|
||||
"sudo snap connect vocalinux:raw-input"
|
||||
"sudo snap connect vocalinux:raw-input\n"
|
||||
"sudo snap connect vocalinux:hardware-observe"
|
||||
)
|
||||
return (
|
||||
"Add your user to the 'input' group and log out/in:\n"
|
||||
@@ -432,11 +433,13 @@ class EvdevKeyboardBackend(KeyboardBackend):
|
||||
)
|
||||
return None
|
||||
|
||||
# No devices found — snap without raw-input often cannot list anything.
|
||||
# No devices found: snap without raw-input and hardware-observe
|
||||
# often cannot list anything.
|
||||
if in_snap:
|
||||
return (
|
||||
"Snap is missing input-device access. Connect once, then restart:\n"
|
||||
"sudo snap connect vocalinux:raw-input"
|
||||
"sudo snap connect vocalinux:raw-input\n"
|
||||
"sudo snap connect vocalinux:hardware-observe"
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -102,6 +102,7 @@ class KeyboardShortcutManager:
|
||||
logger.warning("=" * 60)
|
||||
logger.warning("Global hotkeys read /dev/input (evdev). Connect once:")
|
||||
logger.warning(" sudo snap connect vocalinux:raw-input")
|
||||
logger.warning(" sudo snap connect vocalinux:hardware-observe")
|
||||
logger.warning("Then restart Vocalinux. Until then, use the system tray.")
|
||||
logger.warning("=" * 60)
|
||||
return
|
||||
|
||||
@@ -394,6 +394,32 @@ class TestEvdevKeyboardBackendPermissionHint:
|
||||
result = backend.get_permission_hint()
|
||||
assert result is None
|
||||
|
||||
@patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False)
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.EVDEV_AVAILABLE", True)
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.find_keyboard_devices")
|
||||
def test_permission_hint_snap_no_devices(self, mock_find_devices):
|
||||
"""Snap with no listed devices should ask for both input plugs."""
|
||||
mock_find_devices.return_value = []
|
||||
backend = EvdevKeyboardBackend()
|
||||
result = backend.get_permission_hint()
|
||||
assert result is not None
|
||||
assert "raw-input" in result
|
||||
assert "hardware-observe" in result
|
||||
|
||||
@patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False)
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.EVDEV_AVAILABLE", True)
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.find_keyboard_devices")
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.InputDevice")
|
||||
def test_permission_hint_snap_permission_denied(self, mock_input_device, mock_find_devices):
|
||||
"""Snap that cannot open /dev/input should ask for both input plugs."""
|
||||
mock_find_devices.return_value = ["/dev/input/event0"]
|
||||
mock_input_device.side_effect = OSError(errno.EACCES, "Permission denied")
|
||||
backend = EvdevKeyboardBackend()
|
||||
result = backend.get_permission_hint()
|
||||
assert result is not None
|
||||
assert "raw-input" in result
|
||||
assert "hardware-observe" in result
|
||||
|
||||
|
||||
class TestEvdevKeyboardBackendStart:
|
||||
"""Test start() method."""
|
||||
|
||||
@@ -354,6 +354,17 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
warning_calls = [str(call) for call in mock_logger.warning.call_args_list]
|
||||
self.assertTrue(any("Permission issue" in str(call) for call in warning_calls))
|
||||
|
||||
def test_unavailable_hints_snap_lists_both_connects(self):
|
||||
"""Snap log banner should list both raw-input and hardware-observe."""
|
||||
self.mock_create_backend.return_value = None
|
||||
with patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False):
|
||||
with patch("vocalinux.ui.keyboard_shortcuts.logger") as mock_logger:
|
||||
KeyboardShortcutManager()
|
||||
|
||||
messages = " ".join(str(call) for call in mock_logger.warning.call_args_list)
|
||||
self.assertIn("raw-input", messages)
|
||||
self.assertIn("hardware-observe", messages)
|
||||
|
||||
|
||||
class TestPynputBackend(unittest.TestCase):
|
||||
"""Test cases for the pynput backend specifically."""
|
||||
|
||||
@@ -24,11 +24,16 @@ def test_snapcraft_recipe_and_gui_assets() -> None:
|
||||
|
||||
plugs = set((doc.get("apps") or {}).get("vocalinux", {}).get("plugs") or [])
|
||||
assert "raw-input" in plugs
|
||||
assert "hardware-observe" in plugs
|
||||
assert "audio-record" in plugs
|
||||
assert "uinput" in plugs
|
||||
stage = doc["parts"]["vocalinux"].get("stage-packages") or []
|
||||
assert "ydotool" in stage
|
||||
|
||||
recipe_text = SNAPCRAFT_YAML.read_text(encoding="utf-8")
|
||||
assert "sudo snap connect vocalinux:raw-input" in recipe_text
|
||||
assert "sudo snap connect vocalinux:hardware-observe" in recipe_text
|
||||
|
||||
assert DESKTOP_FILE.is_file()
|
||||
assert SNAP_PNG.is_file()
|
||||
assert SNAP_PNG.stat().st_size > 0
|
||||
@@ -50,6 +55,16 @@ def test_snap_puts_gnome_platform_first_on_ld_library_path() -> None:
|
||||
assert ld_path.endswith(":$LD_LIBRARY_PATH")
|
||||
|
||||
|
||||
def test_snap_docs_tell_users_to_connect_hardware_observe() -> None:
|
||||
"""Hotkeys need hardware-observe for /proc/bus/input/devices (#857)."""
|
||||
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
install = (REPO_ROOT / "docs" / "INSTALL.md").read_text(encoding="utf-8")
|
||||
snapcraft = SNAPCRAFT_YAML.read_text(encoding="utf-8")
|
||||
for text in (readme, install, snapcraft):
|
||||
assert "sudo snap connect vocalinux:raw-input" in text
|
||||
assert "sudo snap connect vocalinux:hardware-observe" in text
|
||||
|
||||
|
||||
def test_snap_docs_warn_that_0162_has_no_uinput_plug() -> None:
|
||||
"""v0.16.2 edge (rev 7) has no uinput plug; the connect command must not stand alone."""
|
||||
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user