| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- #!/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 argparse
- import os
- import subprocess
- import sys
- try:
- import libevdev
- import pyudev
- except ModuleNotFoundError as e:
- print(f"Error: {e!s}", 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") # noqa: SIM115
- 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 and axes[0x35] != axes[0x00]:
- print_bold(
- f"WARNING: fuzz mismatch ABS_X: {axes[0x00]}, ABS_MT_POSITION_X: {axes[0x35]}"
- )
- if axes[0x36] is not None and axes[0x36] != axes[0x01]:
- print_bold(
- f"WARNING: fuzz mismatch ABS_Y: {axes[0x01]}, ABS_MT_POSITION_Y: {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(f" Checking {what}... ", end="")
- if fuzz is None:
- print("not set")
- elif fuzz == (0, 0):
- print("is zero")
- else:
- print(f"x={fuzz[0]} y={fuzz[1]}")
- 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 value in overrides.values():
- 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 = [
- f" EVDEV_ABS_00={overrides[0x00]}",
- f" EVDEV_ABS_01={overrides[0x01]}",
- ]
- if overrides[0x35] is not None:
- template += [
- f" EVDEV_ABS_35={overrides[0x35]}",
- f" EVDEV_ABS_36={overrides[0x36]}",
- ]
- print(f"Checking in {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(f"Checking in {DEFAULT_HWDB_FILE}... ", 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(f"Probable entry for this device found in line {lineno}:")
- print("\n".join(prefix + entry))
- print()
- print_bold("Suggested new entry for this device:")
- new_entry = []
- for i in range(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?",
- f"2. Open {DEFAULT_HWDB_FILE} and amend the existing entry",
- " 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(f"Running udevadm trigger {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:
- with open(path) as f:
- lines = [l[:-1] for l in f]
- 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
- with open("/sys/class/dmi/id/modalias") as f:
- modalias = f.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( # noqa: FLY002
- (
- "# {} {}",
- "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(f"Full modalias is: {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") # noqa: SIM115
- except FileExistsError:
- yesno = input(f"File {fname} exists, overwrite? [Y/n] ")
- if yesno.lower() == "n":
- return
- fd = open(fname, "w") # noqa: SIM115
- with fd:
- fd.write("# File generated by libinput measure fuzz\n\n")
- fd.write(rule)
- if test_hwdb_entry(device, fuzz):
- print(f"Your hwdb override file is in {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(f"Using {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(f"Error: {e}")
- except InvalidDeviceError as e:
- print(f"Error: {e}")
- except KeyboardInterrupt:
- print("Exited on user request")
- if __name__ == "__main__":
- main(sys.argv)
|