fix(evdev): skip multitouch-only touchpads in keyboard discovery (#917)
* fix(evdev): skip multitouch-only touchpads in keyboard discovery Devices reporting only ABS_MT_POSITION_X/Y (no single-touch ABS_X/Y) evaded the pointer-axis check, so multitouch touchpads like the Goodix GXTP5100 were grabbed and cloned — breaking pointer input while running. Fixes #914 * 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 MT axis bits and any future high bits land correctly on both widths. Give the fallback MT-mock a real key so the MT-axes check is the only thing that can drop it. 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
622d0e054d
commit
ecd4ae8388
@@ -20,6 +20,7 @@ to each registered backend for its own consumption decision.
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import select
|
||||
import threading
|
||||
@@ -253,9 +254,10 @@ def find_keyboard_devices() -> list[str]:
|
||||
"""
|
||||
Find all keyboard input devices.
|
||||
|
||||
Devices that emit pointer motion (REL_X/Y or ABS_X/Y) are excluded:
|
||||
a mouse is not a keyboard, and monitoring one proxies all cursor
|
||||
movement through the reader thread (cursor stutter, #900).
|
||||
Devices that emit pointer motion (REL_X/Y, ABS_X/Y, or multitouch
|
||||
ABS_MT_POSITION_X/Y) are excluded: a mouse is not a keyboard, and
|
||||
monitoring one proxies all cursor movement through the reader thread
|
||||
(cursor stutter, #900; dead touchpad, #914).
|
||||
|
||||
Prefers /proc/bus/input/devices (host installs). Falls back to
|
||||
``evdev.list_devices()`` when that path cannot be read (common under snap
|
||||
@@ -288,44 +290,80 @@ def find_keyboard_devices() -> list[str]:
|
||||
# which showed up as periodic cursor stutter (#900).
|
||||
_POINTER_AXIS_BITS = 0x3
|
||||
|
||||
# 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
|
||||
# shifts where each key code or axis lands in the /proc bitmap.
|
||||
_32BIT_MACHINES = frozenset(
|
||||
{
|
||||
"armv5l",
|
||||
"armv6l",
|
||||
"armv7l",
|
||||
"armv8l",
|
||||
"i386",
|
||||
"i486",
|
||||
"i586",
|
||||
"i686",
|
||||
"mips",
|
||||
"mipsel",
|
||||
"ppc",
|
||||
"riscv32",
|
||||
}
|
||||
)
|
||||
_WORD_BITS = 32 if platform.machine() in _32BIT_MACHINES else 64
|
||||
|
||||
|
||||
def _bitmap_to_int(hex_bitmap: str) -> int:
|
||||
"""Integer value of a /proc capability bitmap, or 0 when unparseable.
|
||||
|
||||
Words print most-significant first; each holds ``_WORD_BITS`` bits.
|
||||
"""
|
||||
value = 0
|
||||
for shift, word in enumerate(reversed(hex_bitmap.split())):
|
||||
try:
|
||||
value |= int(word, 16) << (shift * _WORD_BITS)
|
||||
except ValueError:
|
||||
return 0
|
||||
return value
|
||||
|
||||
|
||||
def _bitmap_has_any_bit(hex_bitmap: str) -> bool:
|
||||
"""True when any word of a /proc capability bitmap has a bit set."""
|
||||
try:
|
||||
return any(int(word, 16) for word in hex_bitmap.split())
|
||||
except ValueError:
|
||||
return False
|
||||
"""True when the /proc capability bitmap has a bit set."""
|
||||
return _bitmap_to_int(hex_bitmap) != 0
|
||||
|
||||
|
||||
def _bitmap_low_bits(hex_bitmap: str) -> int:
|
||||
"""Low word (bits 0-63) of a /proc capability bitmap, or 0.
|
||||
"""Bits 0-63 of a /proc capability bitmap, or 0.
|
||||
|
||||
Bitmaps print most-significant word first, so axis bits 0..63 live in
|
||||
the last word on the line.
|
||||
On 64-bit kernels that is the last word; on 32-bit kernels it spans
|
||||
the last two — ``_bitmap_to_int`` handles both.
|
||||
"""
|
||||
words = hex_bitmap.split()
|
||||
if not words:
|
||||
return 0
|
||||
try:
|
||||
return int(words[-1], 16)
|
||||
except ValueError:
|
||||
return 0
|
||||
return _bitmap_to_int(hex_bitmap) & ((1 << 64) - 1)
|
||||
|
||||
|
||||
def _proc_block_has_pointer_axes(device: dict[str, Any]) -> bool:
|
||||
"""True when a parsed /proc device block reports pointer motion axes."""
|
||||
abs_bits = _bitmap_low_bits(device.get("abs", ""))
|
||||
return (
|
||||
_bitmap_low_bits(device.get("rel", "")) & _POINTER_AXIS_BITS == _POINTER_AXIS_BITS
|
||||
or _bitmap_low_bits(device.get("abs", "")) & _POINTER_AXIS_BITS == _POINTER_AXIS_BITS
|
||||
or abs_bits & _POINTER_AXIS_BITS == _POINTER_AXIS_BITS
|
||||
or abs_bits & _POINTER_MT_AXIS_BITS == _POINTER_MT_AXIS_BITS
|
||||
)
|
||||
|
||||
|
||||
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 or ABS_X/Y) are skipped:
|
||||
mice and tablets report EV_KEY bits for their buttons, but grabbing
|
||||
them proxies every cursor movement through this reader.
|
||||
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
|
||||
@@ -370,7 +408,12 @@ def _parse_keyboard_devices_from_proc(proc_file: TextIO) -> list[str]:
|
||||
|
||||
|
||||
def _capabilities_have_pointer_axes(capabilities: Mapping[int, Sequence[int]]) -> bool:
|
||||
"""True when the device reports REL_X+REL_Y or ABS_X+ABS_Y pointer axes."""
|
||||
"""True when the device reports pointer motion axes.
|
||||
|
||||
Covers REL_X+REL_Y, single-touch ABS_X+ABS_Y, and multitouch-only
|
||||
ABS_MT_POSITION_X+ABS_MT_POSITION_Y (touchpads/touchscreens that
|
||||
report no single-touch axes).
|
||||
"""
|
||||
rel_codes = capabilities.get(ecodes.EV_REL, ())
|
||||
if ecodes.REL_X in rel_codes and ecodes.REL_Y in rel_codes:
|
||||
return True
|
||||
@@ -379,7 +422,9 @@ def _capabilities_have_pointer_axes(capabilities: Mapping[int, Sequence[int]]) -
|
||||
entry[0] if isinstance(entry, (tuple, list)) else entry
|
||||
for entry in capabilities.get(ecodes.EV_ABS, ())
|
||||
}
|
||||
return ecodes.ABS_X in abs_codes and ecodes.ABS_Y in abs_codes
|
||||
if ecodes.ABS_X in abs_codes and ecodes.ABS_Y in abs_codes:
|
||||
return True
|
||||
return ecodes.ABS_MT_POSITION_X in abs_codes and ecodes.ABS_MT_POSITION_Y in abs_codes
|
||||
|
||||
|
||||
def _find_keyboard_devices_from_evdev() -> list[str]:
|
||||
|
||||
@@ -202,6 +202,49 @@ B: REL=0 0 143
|
||||
result = find_keyboard_devices()
|
||||
assert result == []
|
||||
|
||||
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
|
||||
N: Name="GXTP5100:00 27C6:01E9"
|
||||
H: Handlers=mouse0 event2
|
||||
B: PROP=85
|
||||
B: EV=1b
|
||||
B: KEY=10000 0 0 0 0
|
||||
B: ABS=60000000000000
|
||||
B: MSC=10
|
||||
"""
|
||||
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_mixed_single_and_mt_touchpad(self) -> None:
|
||||
"""A touchpad with both ABS_X/Y and MT axes stays skipped."""
|
||||
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: ABS=60000000000003
|
||||
"""
|
||||
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_mt_axes_32bit_words(self) -> None:
|
||||
"""On a 32-bit kernel the same MT axes print as two 32-bit words."""
|
||||
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: ABS=600000 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 == []
|
||||
|
||||
|
||||
class TestFindKeyboardDevicesFromEvdev:
|
||||
"""Test _find_keyboard_devices_from_evdev() discovery and filtering."""
|
||||
@@ -245,6 +288,15 @@ class TestFindKeyboardDevicesFromEvdev:
|
||||
ecodes.EV_KEY: [ecodes.KEY_A],
|
||||
ecodes.EV_REL: [ecodes.REL_WHEEL],
|
||||
},
|
||||
# Multitouch-only touchpad: MT position axes, no ABS_X/Y (#914).
|
||||
# Carries a real key so only the MT check can drop it.
|
||||
"/dev/input/event4": {
|
||||
ecodes.EV_KEY: [ecodes.KEY_LEFTCTRL, ecodes.BTN_LEFT],
|
||||
ecodes.EV_ABS: [
|
||||
(ecodes.ABS_MT_POSITION_X, MagicMock()),
|
||||
(ecodes.ABS_MT_POSITION_Y, MagicMock()),
|
||||
],
|
||||
},
|
||||
}
|
||||
devices = self._devices(caps)
|
||||
mock_evdev.list_devices.return_value = list(caps)
|
||||
|
||||
Reference in New Issue
Block a user