[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