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