mirror of
https://gitlab.freedesktop.org/libinput/libinput.git
synced 2025-12-20 09:10:04 +01:00
one second isn't always enough here. Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
462 lines
16 KiB
Python
Executable file
462 lines
16 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# vim: set expandtab shiftwidth=4:
|
|
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
|
|
#
|
|
# Copyright © 2018 Red Hat, Inc.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the 'Software'),
|
|
# to deal in the Software without restriction, including without limitation
|
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
# and/or sell copies of the Software, and to permit persons to whom the
|
|
# Software is furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice (including the next
|
|
# paragraph) shall be included in all copies or substantial portions of the
|
|
# Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
# DEALINGS IN THE SOFTWARE.
|
|
#
|
|
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import subprocess
|
|
try:
|
|
import libevdev
|
|
import pyudev
|
|
except ModuleNotFoundError as e:
|
|
print('Error: {}'.format(str(e)), file=sys.stderr)
|
|
print('One or more python modules are missing. Please install those '
|
|
'modules and re-run this tool.')
|
|
sys.exit(1)
|
|
|
|
|
|
DEFAULT_HWDB_FILE = '/usr/lib/udev/hwdb.d/60-evdev.hwdb'
|
|
OVERRIDE_HWDB_FILE = '/etc/udev/hwdb.d/99-touchpad-fuzz-override.hwdb'
|
|
|
|
|
|
class tcolors:
|
|
GREEN = '\033[92m'
|
|
RED = '\033[91m'
|
|
YELLOW = '\033[93m'
|
|
BOLD = '\033[1m'
|
|
NORMAL = '\033[0m'
|
|
|
|
|
|
def print_bold(msg, **kwargs):
|
|
print(tcolors.BOLD + msg + tcolors.NORMAL, **kwargs)
|
|
|
|
|
|
def print_green(msg, **kwargs):
|
|
print(tcolors.BOLD + tcolors.GREEN + msg + tcolors.NORMAL, **kwargs)
|
|
|
|
|
|
def print_yellow(msg, **kwargs):
|
|
print(tcolors.BOLD + tcolors.YELLOW + msg + tcolors.NORMAL, **kwargs)
|
|
|
|
|
|
def print_red(msg, **kwargs):
|
|
print(tcolors.BOLD + tcolors.RED + msg + tcolors.NORMAL, **kwargs)
|
|
|
|
|
|
class InvalidConfigurationError(Exception):
|
|
pass
|
|
|
|
|
|
class InvalidDeviceError(Exception):
|
|
pass
|
|
|
|
|
|
class Device(libevdev.Device):
|
|
def __init__(self, path):
|
|
if path is None:
|
|
self.path = self.find_touch_device()
|
|
else:
|
|
self.path = path
|
|
|
|
fd = open(self.path, 'rb')
|
|
super().__init__(fd)
|
|
context = pyudev.Context()
|
|
self.udev_device = pyudev.Devices.from_device_file(context, self.path)
|
|
|
|
def find_touch_device(self):
|
|
context = pyudev.Context()
|
|
for device in context.list_devices(subsystem='input'):
|
|
if not device.get('ID_INPUT_TOUCHPAD', 0):
|
|
continue
|
|
|
|
if not device.device_node or \
|
|
not device.device_node.startswith('/dev/input/event'):
|
|
continue
|
|
|
|
return device.device_node
|
|
|
|
print('Unable to find a touch device.', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def check_property(self):
|
|
'''Return a tuple of (xfuzz, yfuzz) with the fuzz as set in the libinput
|
|
property. Returns None if the property doesn't exist'''
|
|
|
|
axes = {
|
|
0x00: self.udev_device.get('LIBINPUT_FUZZ_00'),
|
|
0x01: self.udev_device.get('LIBINPUT_FUZZ_01'),
|
|
0x35: self.udev_device.get('LIBINPUT_FUZZ_35'),
|
|
0x36: self.udev_device.get('LIBINPUT_FUZZ_36'),
|
|
}
|
|
|
|
if axes[0x35] is not None:
|
|
if axes[0x35] != axes[0x00]:
|
|
print_bold('WARNING: fuzz mismatch ABS_X: {}, ABS_MT_POSITION_X: {}'.format(axes[0x00], axes[0x35]))
|
|
|
|
if axes[0x36] is not None:
|
|
if axes[0x36] != axes[0x01]:
|
|
print_bold('WARNING: fuzz mismatch ABS_Y: {}, ABS_MT_POSITION_Y: {}'.format(axes[0x01], axes[0x36]))
|
|
|
|
xfuzz = axes[0x35] or axes[0x00]
|
|
yfuzz = axes[0x36] or axes[0x01]
|
|
|
|
if xfuzz is None and yfuzz is None:
|
|
return None
|
|
|
|
if ((xfuzz is not None and yfuzz is None) or
|
|
(xfuzz is None and yfuzz is not None)):
|
|
raise InvalidConfigurationError('fuzz should be set for both axes')
|
|
|
|
return (int(xfuzz), int(yfuzz))
|
|
|
|
def check_axes(self):
|
|
'''
|
|
Returns a tuple of (xfuzz, yfuzz) with the fuzz as set on the device
|
|
axis. Returns None if no fuzz is set.
|
|
'''
|
|
if not self.has(libevdev.EV_ABS.ABS_X) or not self.has(libevdev.EV_ABS.ABS_Y):
|
|
raise InvalidDeviceError('device does not have x/y axes')
|
|
|
|
if self.has(libevdev.EV_ABS.ABS_MT_POSITION_X) != self.has(libevdev.EV_ABS.ABS_MT_POSITION_Y):
|
|
raise InvalidDeviceError('device does not have both multitouch axes')
|
|
|
|
xfuzz = (self.absinfo[libevdev.EV_ABS.ABS_X].fuzz or
|
|
self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X].fuzz)
|
|
yfuzz = (self.absinfo[libevdev.EV_ABS.ABS_Y].fuzz or
|
|
self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_Y].fuzz)
|
|
|
|
if xfuzz == 0 and yfuzz == 0:
|
|
return None
|
|
|
|
return (xfuzz, yfuzz)
|
|
|
|
|
|
def print_fuzz(what, fuzz):
|
|
print(' Checking {}... '.format(what), end='')
|
|
if fuzz is None:
|
|
print('not set')
|
|
elif fuzz == (0, 0):
|
|
print('is zero')
|
|
else:
|
|
print('x={} y={}'.format(*fuzz))
|
|
|
|
|
|
def handle_existing_entry(device, fuzz):
|
|
# This is getting messy because we don't really know where the entry
|
|
# could be or how the match rule looks like. So we just check the
|
|
# default location only.
|
|
# For the match comparison, we search for the property value in the
|
|
# file. If there is more than one entry that uses the same
|
|
# overrides this will generate false positives.
|
|
# If the lines aren't in the same order in the file, it'll be a false
|
|
# negative.
|
|
overrides = {
|
|
0x00: device.udev_device.get('EVDEV_ABS_00'),
|
|
0x01: device.udev_device.get('EVDEV_ABS_01'),
|
|
0x35: device.udev_device.get('EVDEV_ABS_35'),
|
|
0x36: device.udev_device.get('EVDEV_ABS_36'),
|
|
}
|
|
|
|
has_existing_rules = False
|
|
for key, value in overrides.items():
|
|
if value is not None:
|
|
has_existing_rules = True
|
|
break
|
|
if not has_existing_rules:
|
|
return False
|
|
|
|
print_red('Error! ', end='')
|
|
print('This device already has axis overrides defined')
|
|
print('')
|
|
print_bold('Searching for existing override...')
|
|
|
|
# Construct a template that looks like a hwdb entry (values only) from
|
|
# the udev property values
|
|
template = [' EVDEV_ABS_00={}'.format(overrides[0x00]),
|
|
' EVDEV_ABS_01={}'.format(overrides[0x01])]
|
|
if overrides[0x35] is not None:
|
|
template += [' EVDEV_ABS_35={}'.format(overrides[0x35]),
|
|
' EVDEV_ABS_36={}'.format(overrides[0x36])]
|
|
|
|
print('Checking in {}... '.format(OVERRIDE_HWDB_FILE), end='')
|
|
entry, prefix, lineno = check_file_for_lines(OVERRIDE_HWDB_FILE, template)
|
|
if entry is not None:
|
|
print_green('found')
|
|
print('The existing hwdb entry can be overwritten')
|
|
return False
|
|
else:
|
|
print_red('not found')
|
|
print('Checking in {}... '.format(DEFAULT_HWDB_FILE, template), end='')
|
|
entry, prefix, lineno = check_file_for_lines(DEFAULT_HWDB_FILE, template)
|
|
if entry is not None:
|
|
print_green('found')
|
|
else:
|
|
print_red('not found')
|
|
print('The device has a hwdb override defined but it\'s not where I expected it to be.')
|
|
print('Please look at the libinput documentation for more details.')
|
|
print('Exiting now.')
|
|
return True
|
|
|
|
print_bold('Probable entry for this device found in line {}:'.format(lineno))
|
|
print('\n'.join(prefix + entry))
|
|
print('')
|
|
|
|
print_bold('Suggested new entry for this device:')
|
|
new_entry = []
|
|
for i in range(0, len(template)):
|
|
parts = entry[i].split(':')
|
|
while len(parts) < 4:
|
|
parts.append('')
|
|
parts[3] = str(fuzz)
|
|
new_entry.append(':'.join(parts))
|
|
print('\n'.join(prefix + new_entry))
|
|
print('')
|
|
|
|
# Not going to overwrite the 60-evdev.hwdb entry with this program, too
|
|
# risky. And it may not be our device match anyway.
|
|
print_bold('You must now:')
|
|
print('\n'.join((
|
|
'1. Check the above suggestion for sanity. Does it match your device?',
|
|
'2. Open {} and amend the existing entry'.format(DEFAULT_HWDB_FILE),
|
|
' as recommended above',
|
|
'',
|
|
' The property format is:',
|
|
' EVDEV_ABS_00=min:max:resolution:fuzz',
|
|
'',
|
|
' Leave the entry as-is and only add or amend the fuzz value.',
|
|
' A non-existent value can be skipped, e.g. this entry sets the ',
|
|
' resolution to 32 and the fuzz to 8',
|
|
' EVDEV_ABS_00=::32:8',
|
|
'',
|
|
'3. Save the edited file',
|
|
'4. Say Y to the next prompt')))
|
|
|
|
cont = input('Continue? [Y/n] ')
|
|
if cont == 'n':
|
|
raise KeyboardInterrupt
|
|
|
|
if test_hwdb_entry(device, fuzz):
|
|
print_bold('Please test the new fuzz setting by restarting libinput')
|
|
print_bold('Then submit a pull request for this hwdb entry change to '
|
|
'to systemd at http://github.com/systemd/systemd')
|
|
else:
|
|
print_bold('The new fuzz setting did not take effect.')
|
|
print_bold('Did you edit the correct file?')
|
|
print('Please look at the libinput documentation for more details.')
|
|
print('Exiting now.')
|
|
|
|
return True
|
|
|
|
|
|
def reload_and_trigger_udev(device):
|
|
import time
|
|
|
|
print('Running systemd-hwdb update')
|
|
subprocess.run(['systemd-hwdb', 'update'], check=True)
|
|
syspath = device.path.replace('/dev/input/', '/sys/class/input/')
|
|
time.sleep(2)
|
|
print('Running udevadm trigger {}'.format(syspath))
|
|
subprocess.run(['udevadm', 'trigger', syspath], check=True)
|
|
time.sleep(2)
|
|
|
|
|
|
def test_hwdb_entry(device, fuzz):
|
|
reload_and_trigger_udev(device)
|
|
print_bold('Testing... ', end='')
|
|
|
|
d = Device(device.path)
|
|
f = d.check_axes()
|
|
if f is not None:
|
|
if f == (fuzz, fuzz):
|
|
print_yellow('Warning')
|
|
print_bold('The hwdb applied to the device but libinput\'s udev '
|
|
'rules have not picked it up. This should only happen'
|
|
'if libinput is not installed')
|
|
return True
|
|
else:
|
|
print_red('Error')
|
|
return False
|
|
else:
|
|
f = d.check_property()
|
|
if f is not None and f == (fuzz, fuzz):
|
|
print_green('Success')
|
|
return True
|
|
else:
|
|
print_red('Error')
|
|
return False
|
|
|
|
|
|
def check_file_for_lines(path, template):
|
|
'''
|
|
Checks file at path for the lines given in template. If found, the
|
|
return value is a tuple of the matching lines and the prefix (i.e. the
|
|
two lines before the matching lines)
|
|
'''
|
|
try:
|
|
lines = [l[:-1] for l in open(path).readlines()]
|
|
idx = -1
|
|
try:
|
|
while idx < len(lines) - 1:
|
|
idx += 1
|
|
line = lines[idx]
|
|
if not line.startswith(' EVDEV_ABS_00'):
|
|
continue
|
|
if lines[idx:idx + len(template)] != template:
|
|
continue
|
|
|
|
return (lines[idx:idx + len(template)], lines[idx - 2:idx], idx)
|
|
|
|
except IndexError:
|
|
pass
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
return (None, None, None)
|
|
|
|
|
|
def write_udev_rule(device, fuzz):
|
|
'''Write out a udev rule that may match the device, run udevadm trigger and
|
|
check if the udev rule worked. Of course, there's plenty to go wrong...
|
|
'''
|
|
print('')
|
|
print_bold('Guessing a udev rule to overwrite the fuzz')
|
|
|
|
# Some devices match better on pvr, others on pn, so we get to try both. yay
|
|
modalias = open('/sys/class/dmi/id/modalias').readlines()[0]
|
|
ms = modalias.split(':')
|
|
svn, pn, pvr = None, None, None
|
|
for m in ms:
|
|
if m.startswith('svn'):
|
|
svn = m
|
|
elif m.startswith('pn'):
|
|
pn = m
|
|
elif m.startswith('pvr'):
|
|
pvr = m
|
|
|
|
# Let's print out both to inform and/or confuse the user
|
|
template = '\n'.join(('# {} {}',
|
|
'evdev:name:{}:dmi:*:{}*:{}*:',
|
|
' EVDEV_ABS_00=:::{}',
|
|
' EVDEV_ABS_01=:::{}',
|
|
' EVDEV_ABS_35=:::{}',
|
|
' EVDEV_ABS_36=:::{}',
|
|
''))
|
|
rule1 = template.format(svn[3:], device.name, device.name, svn, pvr, fuzz, fuzz, fuzz, fuzz)
|
|
rule2 = template.format(svn[3:], device.name, device.name, svn, pn, fuzz, fuzz, fuzz, fuzz)
|
|
|
|
print('Full modalias is: {}'.format(modalias))
|
|
print()
|
|
print_bold('Suggested udev rule, option 1:')
|
|
print(rule1)
|
|
print()
|
|
print_bold('Suggested udev rule, option 2:')
|
|
print(rule2)
|
|
print('')
|
|
|
|
# The weird hwdb matching behavior means we match on the least specific
|
|
# rule (i.e. most wildcards) first although that was supposed to be fixed in
|
|
# systemd 3a04b789c6f1.
|
|
# Our rule uses dmi strings and will be more specific than what 60-evdev.hwdb
|
|
# already has. So we basically throw up our hands because we can't do anything
|
|
# then.
|
|
if handle_existing_entry(device, fuzz):
|
|
return
|
|
|
|
while True:
|
|
print_bold('Wich rule do you want to to test? 1 or 2? ', end='')
|
|
yesno = input('Ctrl+C to exit ')
|
|
|
|
if yesno == '1':
|
|
rule = rule1
|
|
break
|
|
elif yesno == '2':
|
|
rule = rule2
|
|
break
|
|
|
|
fname = OVERRIDE_HWDB_FILE
|
|
try:
|
|
fd = open(fname, 'x')
|
|
except FileExistsError:
|
|
yesno = input('File {} exists, overwrite? [Y/n] '.format(fname))
|
|
if yesno.lower == 'n':
|
|
return
|
|
|
|
fd = open(fname, 'w')
|
|
|
|
fd.write('# File generated by libinput measure fuzz\n\n')
|
|
fd.write(rule)
|
|
fd.close()
|
|
|
|
if test_hwdb_entry(device, fuzz):
|
|
print('Your hwdb override file is in {}'.format(fname))
|
|
print_bold('Please test the new fuzz setting by restarting libinput')
|
|
print_bold('Then submit a pull request for this hwdb entry to '
|
|
'systemd at http://github.com/systemd/systemd')
|
|
else:
|
|
print('The hwdb entry failed to apply to the device.')
|
|
print('Removing hwdb file again.')
|
|
os.remove(fname)
|
|
reload_and_trigger_udev(device)
|
|
print_bold('What now?')
|
|
print('1. Re-run this program and try the other suggested udev rule. If that fails,')
|
|
print('2. File a bug with the suggested udev rule at http://github.com/systemd/systemd')
|
|
|
|
|
|
def main(args):
|
|
parser = argparse.ArgumentParser(
|
|
description='Print fuzz settings and/or suggest udev rules for the fuzz to be adjusted.'
|
|
)
|
|
parser.add_argument('path', metavar='/dev/input/event0',
|
|
nargs='?', type=str, help='Path to device (optional)')
|
|
parser.add_argument('--fuzz', type=int, help='Suggested fuzz')
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
device = Device(args.path)
|
|
print_bold('Using {}: {}'.format(device.name, device.path))
|
|
|
|
fuzz = device.check_property()
|
|
print_fuzz('udev property', fuzz)
|
|
|
|
fuzz = device.check_axes()
|
|
print_fuzz('axes', fuzz)
|
|
|
|
userfuzz = args.fuzz
|
|
if userfuzz is not None:
|
|
write_udev_rule(device, userfuzz)
|
|
|
|
except PermissionError:
|
|
print('Permission denied, please re-run as root')
|
|
except InvalidConfigurationError as e:
|
|
print('Error: {}'.format(e))
|
|
except InvalidDeviceError as e:
|
|
print('Error: {}'.format(e))
|
|
except KeyboardInterrupt:
|
|
print('Exited on user request')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|