fix(evdev): require a real keyboard key in /proc keyboard discovery (#918)
* fix(evdev): require a real keyboard key in /proc keyboard discovery Any nonzero B: KEY bit qualified a device as a keyboard, so power/sleep buttons, the ACPI video bus, vendor hotkey blocks and pointer-button interfaces were all grabbed and given (vocalinux) uinput clones, leaving duplicate logical devices that confuse compositor input settings. Require a modifier or KEY_A in the KEY bitmap — the same rule the evdev fallback path already applies — and update the test fixtures to a real keyboard bitmap so they exercise the intended gate. Fixes #915 * fix(evdev): read /proc bitmaps at the kernel's word width Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels (armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int() sized by platform.machine() so high key codes like KEY_RIGHTMETA land correctly on both widths. Give pointer-test mocks real keyboard keys so the pointer check — not the keyboard-key gate — drops them. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Jatin K Malik
Jatin K Malik
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
ecd4ae8388
commit
06f475cf95
@@ -290,12 +290,29 @@ def find_keyboard_devices() -> list[str]:
|
||||
# which showed up as periodic cursor stutter (#900).
|
||||
_POINTER_AXIS_BITS = 0x3
|
||||
|
||||
# Key codes only real keyboards report: the modifiers plus KEY_A — the
|
||||
# same set the evdev fallback requires. Button-only devices (power/sleep
|
||||
# buttons, the ACPI video bus, vendor hotkey blocks) never have one, so
|
||||
# they stop being grabbed and cloned too (#915).
|
||||
_KEYBOARD_KEY_CODES = frozenset(
|
||||
{
|
||||
KEY_LEFTCTRL,
|
||||
KEY_RIGHTCTRL,
|
||||
KEY_LEFTALT,
|
||||
KEY_RIGHTALT,
|
||||
KEY_LEFTSHIFT,
|
||||
KEY_RIGHTSHIFT,
|
||||
KEY_LEFTMETA,
|
||||
KEY_RIGHTMETA,
|
||||
30, # KEY_A
|
||||
}
|
||||
)
|
||||
|
||||
# Bits 53 and 54 of an ABS bitmap: ABS_MT_POSITION_X|ABS_MT_POSITION_Y.
|
||||
# Multitouch-only touchpads and touchscreens (e.g. the Goodix GXTP5100 in
|
||||
# #914) can report the MT pair without the single-touch ABS_X/Y pair.
|
||||
_POINTER_MT_AXIS_BITS = 0x3 << 53
|
||||
|
||||
|
||||
# Machines whose kernels print bitmap words as 32-bit unsigned longs;
|
||||
# everything else (x86_64, aarch64, ppc64*, riscv64, s390x, ...) is 64-bit.
|
||||
# On a 32-bit kernel bit N lives in a different word than on 64-bit, which
|
||||
@@ -338,6 +355,11 @@ def _bitmap_has_any_bit(hex_bitmap: str) -> bool:
|
||||
return _bitmap_to_int(hex_bitmap) != 0
|
||||
|
||||
|
||||
def _bitmap_has_code(hex_bitmap: str, code: int) -> bool:
|
||||
"""True when bit ``code`` is set in a /proc capability bitmap."""
|
||||
return bool(_bitmap_to_int(hex_bitmap) & (1 << code))
|
||||
|
||||
|
||||
def _bitmap_low_bits(hex_bitmap: str) -> int:
|
||||
"""Bits 0-63 of a /proc capability bitmap, or 0.
|
||||
|
||||
@@ -357,13 +379,25 @@ def _proc_block_has_pointer_axes(device: dict[str, Any]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _proc_block_has_keyboard_keys(device: dict[str, Any]) -> bool:
|
||||
"""True when the block's KEY bitmap has a real keyboard key.
|
||||
|
||||
Any nonzero KEY bit used to qualify a block as a keyboard — but
|
||||
touchpad buttons, power/sleep keys and ACPI hotkey blocks all report
|
||||
EV_KEY codes too, so every one of them was grabbed and cloned (#915).
|
||||
"""
|
||||
key_bitmap = device.get("key", "")
|
||||
return any(_bitmap_has_code(key_bitmap, code) for code in _KEYBOARD_KEY_CODES)
|
||||
|
||||
|
||||
def _parse_keyboard_devices_from_proc(proc_file: TextIO) -> list[str]:
|
||||
"""Parse an open /proc/bus/input/devices stream for event KEY devices.
|
||||
|
||||
Devices that emit pointer motion (REL_X/Y, ABS_X/Y or
|
||||
ABS_MT_POSITION_X/Y) are skipped: mice and tablets report EV_KEY bits
|
||||
for their buttons, but grabbing them proxies every cursor movement
|
||||
through this reader.
|
||||
Only blocks carrying a real keyboard key (a modifier or KEY_A, the
|
||||
same rule the evdev fallback applies) qualify. Devices that emit
|
||||
pointer motion (REL_X/Y, ABS_X/Y or ABS_MT_POSITION_X/Y) are
|
||||
skipped: mice and tablets report EV_KEY bits for their buttons, but
|
||||
grabbing them proxies every cursor movement through this reader.
|
||||
"""
|
||||
keyboard_devices: list[str] = []
|
||||
device: Optional[dict[str, Any]] = None
|
||||
@@ -372,18 +406,23 @@ def _parse_keyboard_devices_from_proc(proc_file: TextIO) -> list[str]:
|
||||
nonlocal device
|
||||
if device is None:
|
||||
return
|
||||
if _bitmap_has_any_bit(device.get("key", "")):
|
||||
if _proc_block_has_pointer_axes(device):
|
||||
if not _proc_block_has_keyboard_keys(device):
|
||||
if _bitmap_has_any_bit(device.get("key", "")):
|
||||
logger.debug(
|
||||
"Skipping pointer device during keyboard discovery: "
|
||||
"Skipping non-keyboard device during keyboard discovery: "
|
||||
f"{device.get('name', '?')} ({device.get('handlers', [])})"
|
||||
)
|
||||
else:
|
||||
for handler in device.get("handlers", []):
|
||||
if handler.startswith("event"):
|
||||
device_path = f"/dev/input/{handler}"
|
||||
if os.path.exists(device_path):
|
||||
keyboard_devices.append(device_path)
|
||||
elif _proc_block_has_pointer_axes(device):
|
||||
logger.debug(
|
||||
"Skipping pointer device during keyboard discovery: "
|
||||
f"{device.get('name', '?')} ({device.get('handlers', [])})"
|
||||
)
|
||||
else:
|
||||
for handler in device.get("handlers", []):
|
||||
if handler.startswith("event"):
|
||||
device_path = f"/dev/input/{handler}"
|
||||
if os.path.exists(device_path):
|
||||
keyboard_devices.append(device_path)
|
||||
device = None
|
||||
|
||||
with proc_file as f:
|
||||
|
||||
@@ -54,7 +54,7 @@ H: Handlers=
|
||||
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
|
||||
N: Name="AT Translated Set 2 keyboard"
|
||||
H: Handlers=sysrq kbd event0
|
||||
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
"""
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
@@ -66,7 +66,7 @@ B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
|
||||
N: Name="AT Translated Set 2 keyboard"
|
||||
H: Handlers=sysrq kbd event0
|
||||
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
I: Bus=0018 Vendor=04f3 Product=0033 Version=0500
|
||||
N: Name="Elan Touchpad"
|
||||
H: Handlers=mouse0 event1
|
||||
@@ -126,7 +126,7 @@ B: KEY=0
|
||||
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
|
||||
N: Name="Device with no event"
|
||||
H: Handlers=kbd
|
||||
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
"""
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
result = find_keyboard_devices()
|
||||
@@ -138,7 +138,7 @@ B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
|
||||
N: Name="AT Translated Set 2 keyboard"
|
||||
H: Handlers=sysrq kbd event0
|
||||
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
"""
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=False):
|
||||
@@ -150,14 +150,16 @@ B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
|
||||
N: Name="AT Translated Set 2 keyboard"
|
||||
H: Handlers=sysrq kbd event0
|
||||
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
I: Bus=0003 Vendor=046d Product=c08b Version=0110
|
||||
N: Name="Logitech G502 HERO Gaming Mouse"
|
||||
H: Handlers=mouse0 event5
|
||||
B: KEY=1f0000 0 0 0 0
|
||||
B: KEY=1f0000 0 0 0 38000000
|
||||
B: REL=143
|
||||
B: MSC=10
|
||||
"""
|
||||
# KEY carries real keyboard keys (bits 29-31) so only the pointer
|
||||
# check can drop this device.
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
result = find_keyboard_devices()
|
||||
@@ -168,9 +170,11 @@ B: MSC=10
|
||||
mock_proc_content = """I: Bus=0018 Vendor=04f3 Product=0033 Version=0500
|
||||
N: Name="Elan Touchpad"
|
||||
H: Handlers=mouse0 event1
|
||||
B: KEY=ff000000000000 0 0 0
|
||||
B: KEY=ff000000000000 0 0 38000000
|
||||
B: ABS=273000000000003
|
||||
"""
|
||||
# KEY carries real keyboard keys (bits 29-31) so only the pointer
|
||||
# check can drop this device.
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
result = find_keyboard_devices()
|
||||
@@ -181,7 +185,7 @@ B: ABS=273000000000003
|
||||
mock_proc_content = """I: Bus=0005 Vendor=046d Product=b331 Version=0030
|
||||
N: Name="Wireless Keyboard"
|
||||
H: Handlers=kbd event3
|
||||
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
B: REL=100
|
||||
"""
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
@@ -194,14 +198,71 @@ B: REL=100
|
||||
mock_proc_content = """I: Bus=0003 Vendor=046d Product=c52b Version=1201
|
||||
N: Name="Logitech USB Receiver"
|
||||
H: Handlers=mouse0 event4
|
||||
B: KEY=ffff0000 0 0 0 0
|
||||
B: KEY=ffff0000 0 0 0 38000000
|
||||
B: REL=0 0 143
|
||||
"""
|
||||
# KEY carries real keyboard keys (bits 29-31) so only the pointer
|
||||
# check can drop this device.
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
result = find_keyboard_devices()
|
||||
assert result == []
|
||||
|
||||
def test_find_keyboard_devices_skips_button_only_devices(self) -> None:
|
||||
"""Devices whose KEY bitmap has no real keyboard key are not keyboards (#915)."""
|
||||
mock_proc_content = """I: Bus=0019 Vendor=0000 Product=0001 Version=0000
|
||||
N: Name="Power Button"
|
||||
H: Handlers=kbd event4
|
||||
B: KEY=1000000000000 0
|
||||
I: Bus=0019 Vendor=0000 Product=0003 Version=0000
|
||||
N: Name="Sleep Button"
|
||||
H: Handlers=kbd event5
|
||||
B: KEY=4000 0 0
|
||||
I: Bus=0019 Vendor=0000 Product=0006 Version=0000
|
||||
N: Name="Video Bus"
|
||||
H: Handlers=kbd event6
|
||||
B: KEY=3f000300000000 0 0 0 0
|
||||
I: Bus=0019 Vendor=17aa Product=5054 Version=4101
|
||||
N: Name="ThinkPad Extra Buttons"
|
||||
H: Handlers=kbd event7 rfkill
|
||||
B: KEY=40040002000000 c00017 21004000 300600 400028000000 0
|
||||
I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
|
||||
N: Name="AT Translated Set 2 keyboard"
|
||||
H: Handlers=sysrq kbd event0
|
||||
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
|
||||
I: Bus=0003 Vendor=046d Product=c08b Version=0110
|
||||
N: Name="Logitech G502 HERO Gaming Mouse Keyboard"
|
||||
H: Handlers=sysrq kbd event8
|
||||
B: KEY=ff 0 0 0 38000000
|
||||
I: Bus=0006 Vendor=0000 Product=0000 Version=0001
|
||||
N: Name="ydotoold virtual device"
|
||||
H: Handlers=kbd mouse0 event9
|
||||
B: KEY=ffff0000 0 0 38000000
|
||||
B: REL=143
|
||||
"""
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
result = find_keyboard_devices()
|
||||
assert result == ["/dev/input/event0", "/dev/input/event8"]
|
||||
|
||||
def test_find_keyboard_devices_32bit_words(self) -> None:
|
||||
"""On a 32-bit kernel each bitmap word holds 32 bits, not 64.
|
||||
|
||||
KEY_RIGHTCTRL (code 97) sits in the fourth 32-bit word — the first
|
||||
word printed. A 64-bit reading would look for it in word 1 and
|
||||
miss the keyboard entirely.
|
||||
"""
|
||||
mock_proc_content = """I: Bus=0019 Vendor=0000 Product=0001 Version=0000
|
||||
N: Name="GPIO keyboard"
|
||||
H: Handlers=kbd event2
|
||||
B: KEY=2 0 0 0
|
||||
"""
|
||||
with patch("vocalinux.ui.keyboard_backends.evdev_backend._WORD_BITS", 32):
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
result = find_keyboard_devices()
|
||||
assert result == ["/dev/input/event2"]
|
||||
|
||||
def test_find_keyboard_devices_skips_multitouch_only_touchpad(self) -> None:
|
||||
"""A touchpad reporting only MT axes (no ABS_X/Y) is a pointer (#914)."""
|
||||
mock_proc_content = """I: Bus=0018 Vendor=27c6 Product=01e9 Version=0100
|
||||
@@ -209,7 +270,7 @@ N: Name="GXTP5100:00 27C6:01E9"
|
||||
H: Handlers=mouse0 event2
|
||||
B: PROP=85
|
||||
B: EV=1b
|
||||
B: KEY=10000 0 0 0 0
|
||||
B: KEY=10000 0 0 0 38000000
|
||||
B: ABS=60000000000000
|
||||
B: MSC=10
|
||||
"""
|
||||
@@ -223,7 +284,7 @@ B: MSC=10
|
||||
mock_proc_content = """I: Bus=0018 Vendor=27c6 Product=01e9 Version=0100
|
||||
N: Name="GXTP5100:00 27C6:01E9"
|
||||
H: Handlers=mouse0 event2
|
||||
B: KEY=10000 0 0 0 0
|
||||
B: KEY=10000 0 0 0 38000000
|
||||
B: ABS=60000000000003
|
||||
"""
|
||||
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
|
||||
@@ -236,7 +297,7 @@ B: ABS=60000000000003
|
||||
mock_proc_content = """I: Bus=0018 Vendor=27c6 Product=01e9 Version=0100
|
||||
N: Name="GXTP5100:00 27C6:01E9"
|
||||
H: Handlers=mouse0 event2
|
||||
B: KEY=10000 0 0 0 0
|
||||
B: KEY=10000 0 0 0 38000000
|
||||
B: ABS=600000 0
|
||||
"""
|
||||
with patch("vocalinux.ui.keyboard_backends.evdev_backend._WORD_BITS", 32):
|
||||
|
||||
Reference in New Issue
Block a user