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.hutte...@who-t.net>
---
 circle.yml                |   4 +-
 meson.build               |  18 +-
 tools/libinput-replay.c   | 491 ++++++++++++++++++++++++++++++++++++++++++++++
 tools/libinput-replay.man |  39 ++++
 4 files changed, 549 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 377cb970..0ab39af9 100644
--- a/circle.yml
+++ b/circle.yml
@@ -66,7 +66,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
@@ -90,7 +90,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 05c1306b..0e92dd92 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')
@@ -470,10 +472,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..571aa9d0
--- /dev/null
+++ b/tools/libinput-replay.c
@@ -0,0 +1,491 @@
+/*
+ * 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;
+               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;
+
+       JsonParser *parser;
+};
+
+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->parser;
+       JsonNode *root, *node;
+       JsonObject *o;
+       JsonArray *a;
+       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);
+
+       a = json_object_get_array_member(o, "devices");
+       if (!a || json_array_get_length(a) != ctx->ndevices) {
+               fprintf(stderr, "Parser error: invalid devices list\n");
+               goto out;
+       }
+
+       o = json_array_get_object_element(a, idx);
+
+       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)
+{
+       JsonParser *parser = ctx->parser;
+       JsonArray *a[ctx->ndevices];
+       struct sigaction act;
+       JsonNode *root;
+
+       for (size_t i = 0; i < ctx->ndevices; i++) {
+               JsonObject *o;
+               JsonArray *dlist;
+
+               root = json_parser_get_root(parser);
+               o = json_node_get_object(root);
+               dlist = json_object_get_array_member(o, "devices");
+
+               o = json_array_get_object_element(dlist, i);
+               a[i] = json_object_get_array_member(o, "events");
+       }
+
+       act.sa_handler = sighandler;
+       act.sa_flags = SA_RESETHAND;
+       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)
+                       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;
+       JsonNode *root;
+       JsonObject *o;
+       int version;
+
+       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.parser = json_parser_new();
+       if (!ctx.parser) {
+               fprintf(stderr, "Failed to create parser\n");
+               goto out;
+       }
+
+       if (!json_parser_load_from_file(ctx.parser, argv[optind], NULL)) {
+               g_object_unref(ctx.parser);
+               ctx.parser = NULL;
+               fprintf(stderr, "Failed to parse %s. Oops\n", argv[optind]);
+               goto out;
+       }
+
+       root = json_parser_get_root(ctx.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;
+       }
+
+       ctx.ndevices = json_object_get_int_member(o, "ndevices");
+       assert(ctx.ndevices > 0);
+
+       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:
+       if (ctx.parser)
+               g_object_unref(ctx.parser);
+
+       for (size_t i = 0; i < ctx.ndevices; i++) {
+               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..375c0214
--- /dev/null
+++ b/tools/libinput-replay.man
@@ -0,0 +1,39 @@
+.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
+.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 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

Reply via email to