• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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