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.hutte...@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 _______________________________________________ wayland-devel mailing list wayland-devel@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/wayland-devel