1#!/usr/bin/env python3 2# vim: set expandtab shiftwidth=4: 3# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ 4# 5# Copyright © 2018 Red Hat, Inc. 6# 7# Permission is hereby granted, free of charge, to any person obtaining a 8# copy of this software and associated documentation files (the 'Software'), 9# to deal in the Software without restriction, including without limitation 10# the rights to use, copy, modify, merge, publish, distribute, sublicense, 11# and/or sell copies of the Software, and to permit persons to whom the 12# Software is furnished to do so, subject to the following conditions: 13# 14# The above copyright notice and this permission notice (including the next 15# paragraph) shall be included in all copies or substantial portions of the 16# Software. 17# 18# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24# DEALINGS IN THE SOFTWARE. 25# 26 27import os 28import sys 29import argparse 30import subprocess 31try: 32 import libevdev 33 import pyudev 34except ModuleNotFoundError as e: 35 print('Error: {}'.format(str(e)), file=sys.stderr) 36 print('One or more python modules are missing. Please install those ' 37 'modules and re-run this tool.') 38 sys.exit(1) 39 40 41DEFAULT_HWDB_FILE = '/usr/lib/udev/hwdb.d/60-evdev.hwdb' 42OVERRIDE_HWDB_FILE = '/etc/udev/hwdb.d/99-touchpad-fuzz-override.hwdb' 43 44 45class tcolors: 46 GREEN = '\033[92m' 47 RED = '\033[91m' 48 YELLOW = '\033[93m' 49 BOLD = '\033[1m' 50 NORMAL = '\033[0m' 51 52 53def print_bold(msg, **kwargs): 54 print(tcolors.BOLD + msg + tcolors.NORMAL, **kwargs) 55 56 57def print_green(msg, **kwargs): 58 print(tcolors.BOLD + tcolors.GREEN + msg + tcolors.NORMAL, **kwargs) 59 60 61def print_yellow(msg, **kwargs): 62 print(tcolors.BOLD + tcolors.YELLOW + msg + tcolors.NORMAL, **kwargs) 63 64 65def print_red(msg, **kwargs): 66 print(tcolors.BOLD + tcolors.RED + msg + tcolors.NORMAL, **kwargs) 67 68 69class InvalidConfigurationError(Exception): 70 pass 71 72 73class InvalidDeviceError(Exception): 74 pass 75 76 77class Device(libevdev.Device): 78 def __init__(self, path): 79 if path is None: 80 self.path = self.find_touch_device() 81 else: 82 self.path = path 83 84 fd = open(self.path, 'rb') 85 super().__init__(fd) 86 context = pyudev.Context() 87 self.udev_device = pyudev.Devices.from_device_file(context, self.path) 88 89 def find_touch_device(self): 90 context = pyudev.Context() 91 for device in context.list_devices(subsystem='input'): 92 if not device.get('ID_INPUT_TOUCHPAD', 0): 93 continue 94 95 if not device.device_node or \ 96 not device.device_node.startswith('/dev/input/event'): 97 continue 98 99 return device.device_node 100 101 print('Unable to find a touch device.', file=sys.stderr) 102 sys.exit(1) 103 104 def check_property(self): 105 '''Return a tuple of (xfuzz, yfuzz) with the fuzz as set in the libinput 106 property. Returns None if the property doesn't exist''' 107 108 axes = { 109 0x00: self.udev_device.get('LIBINPUT_FUZZ_00'), 110 0x01: self.udev_device.get('LIBINPUT_FUZZ_01'), 111 0x35: self.udev_device.get('LIBINPUT_FUZZ_35'), 112 0x36: self.udev_device.get('LIBINPUT_FUZZ_36'), 113 } 114 115 if axes[0x35] is not None: 116 if axes[0x35] != axes[0x00]: 117 print_bold('WARNING: fuzz mismatch ABS_X: {}, ABS_MT_POSITION_X: {}'.format(axes[0x00], axes[0x35])) 118 119 if axes[0x36] is not None: 120 if axes[0x36] != axes[0x01]: 121 print_bold('WARNING: fuzz mismatch ABS_Y: {}, ABS_MT_POSITION_Y: {}'.format(axes[0x01], axes[0x36])) 122 123 xfuzz = axes[0x35] or axes[0x00] 124 yfuzz = axes[0x36] or axes[0x01] 125 126 if xfuzz is None and yfuzz is None: 127 return None 128 129 if ((xfuzz is not None and yfuzz is None) or 130 (xfuzz is None and yfuzz is not None)): 131 raise InvalidConfigurationError('fuzz should be set for both axes') 132 133 return (int(xfuzz), int(yfuzz)) 134 135 def check_axes(self): 136 ''' 137 Returns a tuple of (xfuzz, yfuzz) with the fuzz as set on the device 138 axis. Returns None if no fuzz is set. 139 ''' 140 if not self.has(libevdev.EV_ABS.ABS_X) or not self.has(libevdev.EV_ABS.ABS_Y): 141 raise InvalidDeviceError('device does not have x/y axes') 142 143 if self.has(libevdev.EV_ABS.ABS_MT_POSITION_X) != self.has(libevdev.EV_ABS.ABS_MT_POSITION_Y): 144 raise InvalidDeviceError('device does not have both multitouch axes') 145 146 xfuzz = (self.absinfo[libevdev.EV_ABS.ABS_X].fuzz or 147 self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X].fuzz) 148 yfuzz = (self.absinfo[libevdev.EV_ABS.ABS_Y].fuzz or 149 self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_Y].fuzz) 150 151 if xfuzz == 0 and yfuzz == 0: 152 return None 153 154 return (xfuzz, yfuzz) 155 156 157def print_fuzz(what, fuzz): 158 print(' Checking {}... '.format(what), end='') 159 if fuzz is None: 160 print('not set') 161 elif fuzz == (0, 0): 162 print('is zero') 163 else: 164 print('x={} y={}'.format(*fuzz)) 165 166 167def handle_existing_entry(device, fuzz): 168 # This is getting messy because we don't really know where the entry 169 # could be or how the match rule looks like. So we just check the 170 # default location only. 171 # For the match comparison, we search for the property value in the 172 # file. If there is more than one entry that uses the same 173 # overrides this will generate false positives. 174 # If the lines aren't in the same order in the file, it'll be a false 175 # negative. 176 overrides = { 177 0x00: device.udev_device.get('EVDEV_ABS_00'), 178 0x01: device.udev_device.get('EVDEV_ABS_01'), 179 0x35: device.udev_device.get('EVDEV_ABS_35'), 180 0x36: device.udev_device.get('EVDEV_ABS_36'), 181 } 182 183 has_existing_rules = False 184 for key, value in overrides.items(): 185 if value is not None: 186 has_existing_rules = True 187 break 188 if not has_existing_rules: 189 return False 190 191 print_red('Error! ', end='') 192 print('This device already has axis overrides defined') 193 print('') 194 print_bold('Searching for existing override...') 195 196 # Construct a template that looks like a hwdb entry (values only) from 197 # the udev property values 198 template = [' EVDEV_ABS_00={}'.format(overrides[0x00]), 199 ' EVDEV_ABS_01={}'.format(overrides[0x01])] 200 if overrides[0x35] is not None: 201 template += [' EVDEV_ABS_35={}'.format(overrides[0x35]), 202 ' EVDEV_ABS_36={}'.format(overrides[0x36])] 203 204 print('Checking in {}... '.format(OVERRIDE_HWDB_FILE), end='') 205 entry, prefix, lineno = check_file_for_lines(OVERRIDE_HWDB_FILE, template) 206 if entry is not None: 207 print_green('found') 208 print('The existing hwdb entry can be overwritten') 209 return False 210 else: 211 print_red('not found') 212 print('Checking in {}... '.format(DEFAULT_HWDB_FILE, template), end='') 213 entry, prefix, lineno = check_file_for_lines(DEFAULT_HWDB_FILE, template) 214 if entry is not None: 215 print_green('found') 216 else: 217 print_red('not found') 218 print('The device has a hwdb override defined but it\'s not where I expected it to be.') 219 print('Please look at the libinput documentation for more details.') 220 print('Exiting now.') 221 return True 222 223 print_bold('Probable entry for this device found in line {}:'.format(lineno)) 224 print('\n'.join(prefix + entry)) 225 print('') 226 227 print_bold('Suggested new entry for this device:') 228 new_entry = [] 229 for i in range(0, len(template)): 230 parts = entry[i].split(':') 231 while len(parts) < 4: 232 parts.append('') 233 parts[3] = str(fuzz) 234 new_entry.append(':'.join(parts)) 235 print('\n'.join(prefix + new_entry)) 236 print('') 237 238 # Not going to overwrite the 60-evdev.hwdb entry with this program, too 239 # risky. And it may not be our device match anyway. 240 print_bold('You must now:') 241 print('\n'.join(( 242 '1. Check the above suggestion for sanity. Does it match your device?', 243 '2. Open {} and amend the existing entry'.format(DEFAULT_HWDB_FILE), 244 ' as recommended above', 245 '', 246 ' The property format is:', 247 ' EVDEV_ABS_00=min:max:resolution:fuzz', 248 '', 249 ' Leave the entry as-is and only add or amend the fuzz value.', 250 ' A non-existent value can be skipped, e.g. this entry sets the ', 251 ' resolution to 32 and the fuzz to 8', 252 ' EVDEV_ABS_00=::32:8', 253 '', 254 '3. Save the edited file', 255 '4. Say Y to the next prompt'))) 256 257 cont = input('Continue? [Y/n] ') 258 if cont == 'n': 259 raise KeyboardInterrupt 260 261 if test_hwdb_entry(device, fuzz): 262 print_bold('Please test the new fuzz setting by restarting libinput') 263 print_bold('Then submit a pull request for this hwdb entry change to ' 264 'to systemd at http://github.com/systemd/systemd') 265 else: 266 print_bold('The new fuzz setting did not take effect.') 267 print_bold('Did you edit the correct file?') 268 print('Please look at the libinput documentation for more details.') 269 print('Exiting now.') 270 271 return True 272 273 274def reload_and_trigger_udev(device): 275 import time 276 277 print('Running systemd-hwdb update') 278 subprocess.run(['systemd-hwdb', 'update'], check=True) 279 syspath = device.path.replace('/dev/input/', '/sys/class/input/') 280 time.sleep(2) 281 print('Running udevadm trigger {}'.format(syspath)) 282 subprocess.run(['udevadm', 'trigger', syspath], check=True) 283 time.sleep(2) 284 285 286def test_hwdb_entry(device, fuzz): 287 reload_and_trigger_udev(device) 288 print_bold('Testing... ', end='') 289 290 d = Device(device.path) 291 f = d.check_axes() 292 if f is not None: 293 if f == (fuzz, fuzz): 294 print_yellow('Warning') 295 print_bold('The hwdb applied to the device but libinput\'s udev ' 296 'rules have not picked it up. This should only happen' 297 'if libinput is not installed') 298 return True 299 else: 300 print_red('Error') 301 return False 302 else: 303 f = d.check_property() 304 if f is not None and f == (fuzz, fuzz): 305 print_green('Success') 306 return True 307 else: 308 print_red('Error') 309 return False 310 311 312def check_file_for_lines(path, template): 313 ''' 314 Checks file at path for the lines given in template. If found, the 315 return value is a tuple of the matching lines and the prefix (i.e. the 316 two lines before the matching lines) 317 ''' 318 try: 319 lines = [l[:-1] for l in open(path).readlines()] 320 idx = -1 321 try: 322 while idx < len(lines) - 1: 323 idx += 1 324 line = lines[idx] 325 if not line.startswith(' EVDEV_ABS_00'): 326 continue 327 if lines[idx:idx + len(template)] != template: 328 continue 329 330 return (lines[idx:idx + len(template)], lines[idx - 2:idx], idx) 331 332 except IndexError: 333 pass 334 except FileNotFoundError: 335 pass 336 337 return (None, None, None) 338 339 340def write_udev_rule(device, fuzz): 341 '''Write out a udev rule that may match the device, run udevadm trigger and 342 check if the udev rule worked. Of course, there's plenty to go wrong... 343 ''' 344 print('') 345 print_bold('Guessing a udev rule to overwrite the fuzz') 346 347 # Some devices match better on pvr, others on pn, so we get to try both. yay 348 modalias = open('/sys/class/dmi/id/modalias').readlines()[0] 349 ms = modalias.split(':') 350 svn, pn, pvr = None, None, None 351 for m in ms: 352 if m.startswith('svn'): 353 svn = m 354 elif m.startswith('pn'): 355 pn = m 356 elif m.startswith('pvr'): 357 pvr = m 358 359 # Let's print out both to inform and/or confuse the user 360 template = '\n'.join(('# {} {}', 361 'evdev:name:{}:dmi:*:{}*:{}*:', 362 ' EVDEV_ABS_00=:::{}', 363 ' EVDEV_ABS_01=:::{}', 364 ' EVDEV_ABS_35=:::{}', 365 ' EVDEV_ABS_36=:::{}', 366 '')) 367 rule1 = template.format(svn[3:], device.name, device.name, svn, pvr, fuzz, fuzz, fuzz, fuzz) 368 rule2 = template.format(svn[3:], device.name, device.name, svn, pn, fuzz, fuzz, fuzz, fuzz) 369 370 print('Full modalias is: {}'.format(modalias)) 371 print() 372 print_bold('Suggested udev rule, option 1:') 373 print(rule1) 374 print() 375 print_bold('Suggested udev rule, option 2:') 376 print(rule2) 377 print('') 378 379 # The weird hwdb matching behavior means we match on the least specific 380 # rule (i.e. most wildcards) first although that was supposed to be fixed in 381 # systemd 3a04b789c6f1. 382 # Our rule uses dmi strings and will be more specific than what 60-evdev.hwdb 383 # already has. So we basically throw up our hands because we can't do anything 384 # then. 385 if handle_existing_entry(device, fuzz): 386 return 387 388 while True: 389 print_bold('Wich rule do you want to to test? 1 or 2? ', end='') 390 yesno = input('Ctrl+C to exit ') 391 392 if yesno == '1': 393 rule = rule1 394 break 395 elif yesno == '2': 396 rule = rule2 397 break 398 399 fname = OVERRIDE_HWDB_FILE 400 try: 401 fd = open(fname, 'x') 402 except FileExistsError: 403 yesno = input('File {} exists, overwrite? [Y/n] '.format(fname)) 404 if yesno.lower == 'n': 405 return 406 407 fd = open(fname, 'w') 408 409 fd.write('# File generated by libinput measure fuzz\n\n') 410 fd.write(rule) 411 fd.close() 412 413 if test_hwdb_entry(device, fuzz): 414 print('Your hwdb override file is in {}'.format(fname)) 415 print_bold('Please test the new fuzz setting by restarting libinput') 416 print_bold('Then submit a pull request for this hwdb entry to ' 417 'systemd at http://github.com/systemd/systemd') 418 else: 419 print('The hwdb entry failed to apply to the device.') 420 print('Removing hwdb file again.') 421 os.remove(fname) 422 reload_and_trigger_udev(device) 423 print_bold('What now?') 424 print('1. Re-run this program and try the other suggested udev rule. If that fails,') 425 print('2. File a bug with the suggested udev rule at http://github.com/systemd/systemd') 426 427 428def main(args): 429 parser = argparse.ArgumentParser( 430 description='Print fuzz settings and/or suggest udev rules for the fuzz to be adjusted.' 431 ) 432 parser.add_argument('path', metavar='/dev/input/event0', 433 nargs='?', type=str, help='Path to device (optional)') 434 parser.add_argument('--fuzz', type=int, help='Suggested fuzz') 435 args = parser.parse_args() 436 437 try: 438 device = Device(args.path) 439 print_bold('Using {}: {}'.format(device.name, device.path)) 440 441 fuzz = device.check_property() 442 print_fuzz('udev property', fuzz) 443 444 fuzz = device.check_axes() 445 print_fuzz('axes', fuzz) 446 447 userfuzz = args.fuzz 448 if userfuzz is not None: 449 write_udev_rule(device, userfuzz) 450 451 except PermissionError: 452 print('Permission denied, please re-run as root') 453 except InvalidConfigurationError as e: 454 print('Error: {}'.format(e)) 455 except InvalidDeviceError as e: 456 print('Error: {}'.format(e)) 457 except KeyboardInterrupt: 458 print('Exited on user request') 459 460 461if __name__ == '__main__': 462 main(sys.argv) 463