[PATCH v3 libinput 2/4] tools: add a libinput-replay tool

Peter Hutterer peter.hutterer at who-t.net
Tue Feb 27 05:00:19 UTC 2018


Similar in style to evemu-play but parses the YAML printed by
libinput-record. Note that this tool requires python-libevdev which is a new
package and may not be packaged by your distribution. Install with pip3 or
alternatively, just ignore libinput-replay, it's a developer tool only anyway.

User-visible differences to evemu-play:
* supports replaying multiple devices at the same time.
* no replaying on a specific device, we can add this if we ever need it
* --verbose prints the event to stdout as we are replaying them. This is
  particularly useful on long recordings - once the bug occurs we can ctrl+c
  and match up the last few lines with the recordings file. This allows us to
  e.g. drop the rest of the file.

Signed-off-by: Peter Hutterer <peter.hutterer at who-t.net>
---
 meson.build               |   9 +++
 tools/libinput-replay     | 190 ++++++++++++++++++++++++++++++++++++++++++++++
 tools/libinput-replay.man |  28 +++++++
 3 files changed, 227 insertions(+)
 create mode 100755 tools/libinput-replay
 create mode 100644 tools/libinput-replay.man

diff --git a/meson.build b/meson.build
index 05c1306b..b96b35f0 100644
--- a/meson.build
+++ b/meson.build
@@ -470,6 +470,15 @@ configure_file(input : 'tools/libinput-record.man',
 	       install_dir : join_paths(get_option('mandir'), 'man1')
 	       )
 
+install_data('tools/libinput-replay',
+	     install_dir : libinput_tool_path)
+configure_file(input : 'tools/libinput-replay.man',
+	       output : 'libinput-replay.1',
+	       configuration : man_config,
+	       install : true,
+	       install_dir : join_paths(get_option('mandir'), 'man1')
+	       )
+
 if get_option('debug-gui')
 	dep_gtk = dependency('gtk+-3.0', version : '>= 3.20')
 	dep_cairo = dependency('cairo')
diff --git a/tools/libinput-replay b/tools/libinput-replay
new file mode 100755
index 00000000..4f012a6b
--- /dev/null
+++ b/tools/libinput-replay
@@ -0,0 +1,190 @@
+#!/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.
+
+try:
+    import argparse
+    import libevdev
+    import multiprocessing
+    import os
+    import sys
+    import time
+    import yaml
+except ModuleNotFoundError as e:
+    print('Error: {}'.format(e), file=sys.stderr)
+    print('One or more python modules are missing. Please install those '
+          'modules and re-run this tool.')
+    sys.exit(1)
+
+
+SUPPORTED_FILE_VERSION = 1
+
+
+def error(msg, **kwargs):
+    print(msg, **kwargs, file=sys.stderr)
+
+
+class YamlException(Exception):
+    pass
+
+
+def fetch(yaml, key):
+    '''Helper function to avoid confusing a YAML error with a
+    normal KeyError bug'''
+    try:
+        return yaml[key]
+    except KeyError:
+        raise YamlException('Failed to get \'{}\' from recording.'.format(key))
+
+
+def create(device):
+    evdev = fetch(device, 'evdev')
+
+    d = libevdev.Device()
+    d.name = fetch(evdev, 'name')
+
+    ids = fetch(evdev, 'id')
+    if len(ids) != 4:
+        raise YamlException('Invalid ID format: {}'.format(ids))
+    d.id = dict(zip(['bustype', 'vendor', 'product', 'version'], ids))
+
+    codes = fetch(evdev, 'codes')
+    for evtype, evcodes in codes.items():
+        for code in evcodes:
+            data = None
+            if evtype == libevdev.EV_ABS.value:
+                values = fetch(evdev, 'absinfo')[code]
+                absinfo = libevdev.InputAbsInfo(minimum=values[0],
+                                                maximum=values[1],
+                                                fuzz=values[2],
+                                                flat=values[3],
+                                                resolution=values[4])
+                data = absinfo
+            elif evtype == libevdev.EV_REP.value:
+                if code == libevdev.EV_REP.REP_DELAY.value:
+                    data = 500
+                elif code == libevdev.EV_REP.REP_PERIOD.value:
+                    data = 20
+            d.enable(libevdev.evbit(evtype, code), data=data)
+
+    uinput = d.create_uinput_device()
+    return uinput
+
+
+def print_events(devnode, indent, evs):
+    devnode = os.path.basename(devnode)
+    for e in evs:
+        print("{}: {}{:06d}.{:06d} {} / {:<20s} {:4d}".format(
+            devnode, ' ' * (indent * 8), e.sec, e.usec, e.type.name, e.code.name, e.value))
+
+
+def replay(device, verbose):
+    events = fetch(device, 'events')
+    if events is None:
+        return
+    uinput = device['__uinput']
+
+    offset = time.time()
+
+    # each 'evdev' set contains one SYN_REPORT so we only need to check for
+    # the time offset once per event
+    for event in events:
+        evdev = fetch(event, 'evdev')
+        (sec, usec, evtype, evcode, value) = evdev[0]
+
+        evtime = sec + usec/1e6 + offset
+        now = time.time()
+
+        if evtime - now > 150/1e6:  # 150 µs error margin
+            time.sleep(evtime - now - 150/1e6)
+
+        evs = [libevdev.InputEvent(libevdev.evbit(e[2], e[3]), value=e[4], sec=e[0], usec=e[1]) for e in evdev]
+        uinput.send_events(evs)
+        print_events(uinput.devnode, device['__index'], evs)
+
+
+def wrap(func, *args):
+    try:
+        func(*args)
+    except KeyboardInterrupt:
+        pass
+
+
+def loop(args, recording):
+    version = fetch(recording, 'version')
+    if version != SUPPORTED_FILE_VERSION:
+        raise YamlException('Invalid file format: {}, expected {}'.format(version, SUPPORTED_FILE_VERSION))
+
+    ndevices = fetch(recording, 'ndevices')
+    devices = fetch(recording, 'devices')
+    if ndevices != len(devices):
+        error('WARNING: truncated file, expected {} devices, got {}'.format(ndevices, len(devices)))
+
+    for idx, d in enumerate(devices):
+        uinput = create(d)
+        print('{}: {}'.format(uinput.devnode, uinput.name))
+        d['__uinput'] = uinput  # cheaper to hide it in the dict then work around it
+        d['__index'] = idx
+
+    stop = False
+    while not stop:
+        input('Hit enter to start replaying')
+
+        processes = []
+        for d in devices:
+            p = multiprocessing.Process(target=wrap, args=(replay, d, args.verbose))
+            processes.append(p)
+
+        for p in processes:
+            p.start()
+
+        for p in processes:
+            p.join()
+
+        del processes
+
+
+def main():
+    parser = argparse.ArgumentParser(
+            description='Replay a device recording'
+    )
+    parser.add_argument('recording', metavar='recorded-file.yaml',
+                        type=str, help='Path to device recording')
+    parser.add_argument('--verbose', action='store_true')
+    args = parser.parse_args()
+
+    try:
+        with open(args.recording) as f:
+            y = yaml.safe_load(f)
+            loop(args, y)
+    except KeyboardInterrupt:
+        pass
+    except (PermissionError, OSError) as e:
+        error('Error: failed to open device: {}'.format(e))
+    except YamlException as e:
+        error('Error: failed to parse recording: {}'.format(e))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tools/libinput-replay.man b/tools/libinput-replay.man
new file mode 100644
index 00000000..7bca5180
--- /dev/null
+++ b/tools/libinput-replay.man
@@ -0,0 +1,28 @@
+.TH libinput-replay "1"
+.SH NAME
+libinput\-replay \- replay kernel events from a recording
+.SH SYNOPSIS
+.B libinput replay [options] \fIrecording\fB
+.SH DESCRIPTION
+.PP
+The \fBlibinput replay\fR tool replays kernel events from a device recording
+made by the \fBlibinput record(1)\fR tool. This tool needs to run as root to
+create a device and/or replay events.
+.PP
+If the recording contains more than one device, all devices are replayed
+simultaneously.
+.SH OPTIONS
+.TP 8
+.B \-\-help
+Print help
+.SH NOTES
+.PP
+This tool replays events from a recording through the the kernel and is
+independent of libinput. In other words, updating or otherwise changing
+libinput will not alter the output from this tool. libinput itself does not
+need to be in use to replay events.
+.SH LIBINPUT
+.PP
+Part of the
+.B libinput(1)
+suite
-- 
2.14.3



More information about the wayland-devel mailing list