[PATCH libinput 2/2] tools: add a libinput-replay tool
Peter Hutterer
peter.hutterer at who-t.net
Tue Nov 28 01:40:50 UTC 2017
Similar in style to evemu-play but parses the JSON printed by
libinput-record.
User-visible differences to evemu-play:
* supports replaying multiple devices at the same time.
* to replay on a specific device, --replay-on is required
* --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>
---
circle.yml | 4 +-
meson.build | 18 +-
tools/libinput-replay.c | 494 ++++++++++++++++++++++++++++++++++++++++++++++
tools/libinput-replay.man | 39 ++++
4 files changed, 552 insertions(+), 3 deletions(-)
create mode 100644 tools/libinput-replay.c
create mode 100644 tools/libinput-replay.man
diff --git a/circle.yml b/circle.yml
index a204dc40..172cafdd 100644
--- a/circle.yml
+++ b/circle.yml
@@ -81,7 +81,7 @@ fedora_install: &fedora_install
name: Install prerequisites
command: |
dnf upgrade -y libsolv
- dnf install -y git gcc gcc-c++ meson check-devel libudev-devel libevdev-devel doxygen graphviz valgrind binutils libwacom-devel cairo-devel gtk3-devel glib2-devel mtdev-devel
+ dnf install -y git gcc gcc-c++ meson check-devel libudev-devel libevdev-devel doxygen graphviz valgrind binutils libwacom-devel cairo-devel gtk3-devel glib2-devel mtdev-devel json-glib-devel
fedora_build_all: &fedora_build_all
<<: *default_settings
@@ -105,7 +105,7 @@ ubuntu_install: &ubuntu_install
apt-get install -y software-properties-common
add-apt-repository universe
apt-get update
- apt-get install -y git gcc g++ meson check libudev-dev libevdev-dev doxygen graphviz valgrind binutils libwacom-dev libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev
+ apt-get install -y git gcc g++ meson check libudev-dev libevdev-dev doxygen graphviz valgrind binutils libwacom-dev libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev libjson-glib-dev
ubuntu_build_all: &ubuntu_build_all
<<: *default_settings
diff --git a/meson.build b/meson.build
index 7a1b0d48..72520219 100644
--- a/meson.build
+++ b/meson.build
@@ -46,6 +46,8 @@ dep_mtdev = dependency('mtdev', version : '>= 1.1.0')
dep_libevdev = dependency('libevdev', version : '>= 0.4')
dep_lm = cc.find_library('m', required : false)
dep_rt = cc.find_library('rt', required : false)
+dep_glib = dependency('glib-2.0')
+dep_json_glib = dependency('json-glib-1.0')
# Include directories
includes_include = include_directories('include')
@@ -467,10 +469,24 @@ configure_file(input : 'tools/libinput-record.man',
install_dir : join_paths(get_option('mandir'), 'man1')
)
+libinput_replay_sources = [ 'tools/libinput-replay.c' ]
+executable('libinput-replay',
+ libinput_replay_sources,
+ dependencies : deps_tools + [dep_glib, dep_json_glib],
+ include_directories : [includes_src, includes_include],
+ install_dir : libinput_tool_path,
+ install : true,
+ )
+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')
- dep_glib = dependency('glib-2.0')
debug_gui_sources = [ 'tools/libinput-debug-gui.c' ]
deps_debug_gui = [
diff --git a/tools/libinput-replay.c b/tools/libinput-replay.c
new file mode 100644
index 00000000..d7e5c75f
--- /dev/null
+++ b/tools/libinput-replay.c
@@ -0,0 +1,494 @@
+/*
+ * Copyright © 2017 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.
+ */
+
+#include "config.h"
+
+#include <libevdev/libevdev.h>
+#include <libevdev/libevdev-uinput.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <glib.h>
+#include <poll.h>
+#include <signal.h>
+#include <json-glib/json-glib.h>
+#include <sys/wait.h>
+
+#include "libinput-util.h"
+
+static bool stop = false;
+
+static void sighandler(int signal)
+{
+ stop = true;
+}
+
+struct replay_context {
+ struct device {
+ const char *device;
+ char *name;
+ struct libevdev_uinput *uinput;
+ JsonParser *parser;
+ int dest; /* fd to the target device or the uinput device */
+ } devices[10];
+ size_t ndevices;
+
+ uint64_t us;
+
+ bool interactive;
+ bool verbose;
+
+ int my_fd;
+};
+
+static void
+parse_prop_array(JsonArray *array,
+ guint index,
+ JsonNode *node,
+ gpointer user_data)
+{
+ struct libevdev *dev = user_data;
+ libevdev_enable_property(dev, (int)json_node_get_int(node));
+}
+
+struct enable_type_data {
+ unsigned int type;
+ struct libevdev *dev;
+};
+
+static void
+parse_type_array(JsonArray *array,
+ guint index,
+ JsonNode *node,
+ gpointer user_data)
+{
+ struct enable_type_data *td = user_data;
+
+ libevdev_enable_event_code(td->dev,
+ td->type,
+ (int)json_node_get_int(node),
+ NULL);
+}
+
+static void
+parse_absinfo_array(JsonArray *array,
+ guint index,
+ JsonNode *node,
+ gpointer user_data)
+{
+ struct libevdev *dev = user_data;
+ JsonArray *a;
+ unsigned int code;
+ struct input_absinfo abs = {0};
+
+ a = json_node_get_array(node);
+ if (json_array_get_length(a) != 6) {
+ fprintf(stderr, "Invalid absinfo array\n");
+ return;
+ }
+
+ code = (int)json_array_get_int_element(a, 0);
+ abs.minimum = (int)json_array_get_int_element(a, 1);
+ abs.maximum = (int)json_array_get_int_element(a, 2);
+ abs.fuzz = (int)json_array_get_int_element(a, 3);
+ abs.flat = (int)json_array_get_int_element(a, 4);
+ abs.resolution = (int)json_array_get_int_element(a, 5);
+ libevdev_enable_event_code(dev, EV_ABS, code, &abs);
+}
+
+static int
+create_device(struct replay_context *ctx, int idx)
+{
+ JsonParser *parser = ctx->devices[idx].parser;
+ JsonNode *root, *node;
+ JsonObject *o;
+ JsonArray *a;
+ gint64 version;
+ const char *str;
+ struct libevdev *dev = NULL;
+ int rc = 1;
+ struct enable_type_data td;
+
+ dev = libevdev_new();
+
+ root = json_parser_get_root(parser);
+ o = json_node_get_object(root);
+
+ version = json_object_get_int_member(o, "version");
+ if (version != 1) {
+ fprintf(stderr, "Parser error: invalid version\n");
+ goto out;
+ }
+
+ node = json_object_get_member(o, "evdev");
+ if (!node) {
+ fprintf(stderr, "Parser error: missing \"evdev\" entry\n");
+ goto out;
+ }
+ o = json_node_get_object(node);
+
+ str = json_object_get_string_member(o, "name");
+ if (!str) {
+ fprintf(stderr, "Parser error: device name missing\n");
+ goto out;
+ }
+ libevdev_set_name(dev, str);
+ ctx->devices[idx].name = strdup(str);
+
+ a = json_object_get_array_member(o, "id");
+ if (!a || json_array_get_length(a) != 4) {
+ fprintf(stderr, "Parser error: invalid id\n");
+ goto out;
+ }
+
+ libevdev_set_id_bustype(dev, (int)json_array_get_int_element(a, 0));
+ libevdev_set_id_vendor(dev, (int)json_array_get_int_element(a, 1));
+ libevdev_set_id_product(dev, (int)json_array_get_int_element(a, 2));
+ libevdev_set_id_version(dev, (int)json_array_get_int_element(a, 3));
+
+ a = json_object_get_array_member(o, "properties");
+ if (!a) {
+ fprintf(stderr,
+ "Parser error: missing \"properties\" entry\n");
+ goto out;
+ }
+ json_array_foreach_element(a, parse_prop_array, dev);
+
+ /* parsing absinfo first means we can ignore the abs list later */
+ a = json_object_get_array_member(o, "absinfo");
+ if (a)
+ json_array_foreach_element(a, parse_absinfo_array, dev);
+
+ /* we don't care about syn, it's always enabled */
+ for (unsigned int type = 0; type < EV_CNT; type++) {
+ const char *key = NULL;
+
+ if (type == EV_SYN || type == EV_ABS)
+ continue;
+
+ switch (type) {
+ case EV_SYN: key = "syn"; break;
+ case EV_KEY: key = "key"; break;
+ case EV_REL: key = "rel"; break;
+ case EV_ABS: key = "abs"; break;
+ case EV_MSC: key = "msc"; break;
+ case EV_SW: key = "sw"; break;
+ case EV_LED: key = "led"; break;
+ case EV_SND: key = "snd"; break;
+ case EV_REP: key = "rep"; break;
+ case EV_FF: key = "ff"; break;
+ case EV_PWR: key = "pwr"; break;
+ case EV_FF_STATUS: key = "ff_status"; break;
+ default:
+ break;
+ }
+
+ if (key == NULL)
+ continue;
+
+ td.dev = dev;
+ td.type = type;
+ if (!json_object_has_member(o, key))
+ continue;
+
+ a = json_object_get_array_member(o, key);
+ if (!a) {
+ fprintf(stderr,
+ "Parser error: entry \"%s\" is invalid",
+ key);
+ goto out;
+ }
+ json_array_foreach_element(a, parse_type_array, &td);
+ }
+
+ rc = libevdev_uinput_create_from_device(dev,
+ LIBEVDEV_UINPUT_OPEN_MANAGED,
+ &ctx->devices[idx].uinput);
+ if (rc != 0) {
+ fprintf(stderr,
+ "Failed to create uinput device (%s)\n",
+ strerror(-rc));
+ goto out;
+
+ }
+ ctx->devices[idx].dest = libevdev_uinput_get_fd(ctx->devices[idx].uinput);
+ rc = 0;
+out:
+ if (dev)
+ libevdev_free(dev);
+ if (rc != 0)
+ free(ctx->devices[idx].name);
+ return rc;
+}
+
+static void
+play(JsonArray *array, guint index, JsonNode *node, gpointer user_data)
+{
+ struct replay_context *ctx = user_data;
+ JsonObject *o;
+ JsonArray *a;
+ struct input_event e;
+ uint64_t etime;
+ unsigned int tdelta;
+ const int ERROR_MARGIN = 150; /* us */
+ int nevents;
+
+ if (stop)
+ return;
+
+ o = json_node_get_object(node);
+
+ if (!json_object_has_member(o, "evdev"))
+ return;
+
+ a = json_object_get_array_member(o, "evdev");
+ nevents = json_array_get_length(a);
+ assert(nevents > 0);
+
+ for (int i = 0; i < nevents; i++) {
+ JsonArray *data;
+
+ o = json_array_get_object_element(a, i);
+ data = json_object_get_array_member(o, "data");
+ if (!data) {
+ fprintf(stderr, "Parser error: missing event data\n");
+ return;
+ }
+
+ e.time.tv_sec = (int)json_array_get_int_element(data, 0);
+ e.time.tv_usec = (int)json_array_get_int_element(data, 1);
+ e.type = (int)json_array_get_int_element(data, 2);
+ e.code = (int)json_array_get_int_element(data, 3);
+ e.value = (int)json_array_get_int_element(data, 4);
+
+ etime = tv2us(&e.time);
+ tdelta = etime - ctx->us;
+ if (tdelta > 0)
+ usleep(tdelta - ERROR_MARGIN);
+ ctx->us = etime;
+
+ write(ctx->my_fd, &e, sizeof(e));
+
+ if (ctx->verbose ) {
+ if (e.type == EV_SYN && e.type != SYN_MT_REPORT) {
+ printf("%03ld.%06u ------------ %s (%d) ----------\n",
+ e.time.tv_sec,
+ (unsigned int)e.time.tv_usec,
+ libevdev_event_code_get_name(e.type, e.code),
+ e.code);
+ } else {
+ printf("%03ld.%06u %s / %-20s %4d\n",
+ e.time.tv_sec,
+ (unsigned int)e.time.tv_usec,
+ libevdev_event_type_get_name(e.type),
+ libevdev_event_code_get_name(e.type, e.code),
+ e.value);
+ }
+ }
+ }
+}
+
+static void
+play_events(struct replay_context *ctx)
+{
+ JsonArray *a[ctx->ndevices];
+ struct sigaction act;
+
+ for (size_t i = 0; i < ctx->ndevices; i++) {
+ JsonParser *parser;
+ JsonNode *root;
+ JsonObject *o;
+ parser = ctx->devices[i].parser;
+ root = json_parser_get_root(parser);
+ o = json_node_get_object(root);
+ a[i] = json_object_get_array_member(o, "events");
+ }
+
+ act.sa_handler = sighandler;
+ act.sa_flags = SA_RESETHAND;
+ sigaction(SIGINT, &act, NULL);
+ sigaction(SIGINT, &act, NULL);
+
+ do {
+ int status;
+
+ if (ctx->interactive) {
+ char line[32];
+ printf("Hit enter to start replaying");
+ fflush(stdout);
+ fgets(line, sizeof(line), stdin);
+ }
+
+ ctx->us = 0;
+
+ for (size_t i = 0; i < ctx->ndevices; i++) {
+ if (fork() == 0) {
+ close(STDIN_FILENO);
+ ctx->my_fd = ctx->devices[i].dest;
+ json_array_foreach_element(a[i], play, ctx);
+ exit(0);
+ }
+ }
+
+ while (wait(&status) != -1) {
+ /* humm dee dumm */
+ }
+ if (errno == ECHILD)
+ break;
+ else
+ fprintf(stderr, "oops. %m\n");
+ } while (ctx->interactive && !stop);
+
+}
+
+static inline void
+usage(void)
+{
+ printf("Usage: %s [--help] recordings-file\n"
+ "For more information, see the %s(1) man page\n",
+ program_invocation_short_name,
+ program_invocation_short_name);
+}
+
+enum options {
+ OPT_DEVICE,
+ OPT_HELP,
+ OPT_INTERACTIVE,
+ OPT_VERBOSE,
+};
+
+int main(int argc, char **argv)
+{
+ struct replay_context ctx = {0};
+ struct option opts[] = {
+ { "replay-on", required_argument, 0, OPT_DEVICE },
+ { "interactive", no_argument, 0, OPT_INTERACTIVE },
+ { "help", no_argument, 0, OPT_HELP },
+ { "verbose", no_argument, 0, OPT_VERBOSE },
+ { 0, 0, 0, 0 },
+ };
+ int rc = 1;
+ const char *device_arg = NULL;
+
+ for (size_t i = 0; i < ARRAY_LENGTH(ctx.devices); i++)
+ ctx.devices[i].dest = -1;
+
+ while (1) {
+ int c;
+ int option_index = 0;
+
+ c = getopt_long(argc, argv, "ho:", opts, &option_index);
+ if (c == -1)
+ break;
+
+ switch (c) {
+ case 'h':
+ case OPT_HELP:
+ usage();
+ rc = 0;
+ goto out;
+ case OPT_VERBOSE:
+ ctx.verbose = true;
+ break;
+ case OPT_DEVICE:
+ device_arg = optarg;
+ break;
+ case OPT_INTERACTIVE:
+ ctx.interactive = true;
+ break;
+ }
+ }
+
+ if (optind >= argc) {
+ usage();
+ goto out;
+ }
+
+ ctx.ndevices = argc - optind;
+ if (ctx.ndevices >= ARRAY_LENGTH(ctx.devices)) {
+ fprintf(stderr,
+ "Number of files must not exceed %zd\n",
+ ARRAY_LENGTH(ctx.devices));
+ goto out;
+ }
+
+ if (ctx.ndevices > 1 && device_arg) {
+ fprintf(stderr,
+ "Option --replay-on can only work with one file\n");
+ goto out;
+ }
+
+ for (size_t i = 0; i < ctx.ndevices; i++) {
+ const char *recording = NULL;
+ JsonParser *parser;
+
+ recording = argv[optind++];
+
+ parser = json_parser_new();
+ if (!parser) {
+ fprintf(stderr, "Failed to create parser\n");
+ goto out;
+ }
+
+ if (!json_parser_load_from_file(parser, recording, NULL)) {
+ g_object_unref(parser);
+ fprintf(stderr, "Failed to parse %s. Oops\n", recording);
+ goto out;
+ }
+ ctx.devices[i].parser = parser;
+ }
+
+ if (device_arg == NULL || ctx.ndevices > 1)
+ ctx.interactive = true;
+
+ if (!device_arg) {
+ for (size_t i = 0; i < ctx.ndevices; i++) {
+ if (create_device(&ctx, i) != 0)
+ goto out;
+ printf("%s: %s\n",
+ ctx.devices[i].name,
+ libevdev_uinput_get_devnode(ctx.devices[i].uinput));
+ }
+ } else {
+ ctx.devices[0].dest = open(device_arg, O_RDWR);
+ if (ctx.devices[0].dest == -1) {
+ fprintf(stderr, "Failed to open %s (%m)\n", device_arg);
+ }
+ }
+
+ /* device is created now */
+ play_events(&ctx);
+
+ rc = 0;
+
+out:
+ for (size_t i = 0; i < ctx.ndevices; i++) {
+ if (ctx.devices[i].parser)
+ g_object_unref(ctx.devices[i].parser);
+ if (ctx.devices[i].uinput)
+ libevdev_uinput_destroy(ctx.devices[i].uinput);
+ free(ctx.devices[i].name);
+ close(ctx.devices[i].dest);
+ }
+
+ return rc;
+}
diff --git a/tools/libinput-replay.man b/tools/libinput-replay.man
new file mode 100644
index 00000000..166be6ff
--- /dev/null
+++ b/tools/libinput-replay.man
@@ -0,0 +1,39 @@
+.TH libinput-replay "1"
+.SH NAME
+libinput\-replay \- replay kernel events
+.SH SYNOPSIS
+.B libinput replay [options] \fIrecording\fB [\fIrecording2\fI ...]
+.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 more than one recording is given, all recordings are replayed at the same
+time.
+.SH OPTIONS
+.TP 8
+.B \-\-help
+Print help
+.TP 8
+.B \-\-interactive
+Before replaying the events, prompt for user input. After replaying events,
+prompt for user input again instead of exiting to allow for replaying the
+same sequence multiple times. This is the default behavior unless
+\fB\-\-replay-on\fR was given.
+.TP 8
+.B \-\-replay-on=\fI/dev/input/event0\fB
+Replay the events in the recording on the given device instead of creating a
+new uinput device. The sequence is replayed immediately and this tool exits
+unless \fB\-\-interactive\fR is given.
+.SH NOTES
+.PP
+This tool replays events from 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.13.6
More information about the wayland-devel
mailing list