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