tools/record: add support for hidraw recording

New commandline switch --with-hidraw. This will open all hidraw devices
associated with this device and add any reports to the output in the
form:

  events:
    - hid:
        time: [0, 0]
        hidraw1: [0x01, 0x02, 0x03, 0x05, 0x06]
	hidraw2: [0x07, 0x08, 0x09, 0x0a, 0x0b]
    - evdev:
      ...

i.e. there's a nesting of `hid` with a list of reports, each with the hidraw
node as dictionary entry.

Because hidraw events do not have timestamps and always occur before the evdev
events, they are in a separate frame (as shown above). We could try to figure
out how to match them with the upcoming evdev frame but it's not worth it for
now.

The timestamp itself is a special key in the hidraw with the timestamp from
clock_gettime.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
This commit is contained in:
Peter Hutterer 2021-02-18 14:20:22 +10:00
parent f1b16c18e5
commit cf1abf0818
4 changed files with 177 additions and 5 deletions

View file

@ -223,6 +223,7 @@ __all_seats()
'--multiple[Record multiple devices at once]' \ '--multiple[Record multiple devices at once]' \
'--show-keycodes[Show keycodes as-is in the recording]' \ '--show-keycodes[Show keycodes as-is in the recording]' \
'--with-libinput[Record libinput events alongside device events]' \ '--with-libinput[Record libinput events alongside device events]' \
'--with-hidraw[Record hidraw events alongside device events]' \
'*::device:_files -W /dev/input/ -P /dev/input/' '*::device:_files -W /dev/input/ -P /dev/input/'
} }

View file

@ -70,7 +70,7 @@ enum indent {
I_LIBINPUTDEV = 4, /* nodes inside libinput: (the I_LIBINPUTDEV = 4, /* nodes inside libinput: (the
device description */ device description */
I_EVENTTYPE = 4, /* event type (evdev:, libinput:, I_EVENTTYPE = 4, /* event type (evdev:, libinput:,
...) */ hidraw:) */
I_EVENT = 6, /* event data */ I_EVENT = 6, /* event data */
}; };
@ -82,6 +82,7 @@ struct record_device {
struct libevdev *evdev_prev; /* previous value, used for EV_ABS struct libevdev *evdev_prev; /* previous value, used for EV_ABS
deltas */ deltas */
struct libinput_device *device; struct libinput_device *device;
struct list hidraw_devices;
struct { struct {
bool is_touch_device; bool is_touch_device;
@ -92,6 +93,13 @@ struct record_device {
FILE *fp; FILE *fp;
}; };
struct hidraw {
struct list link;
struct record_device *device;
int fd;
char *name;
};
struct record_context { struct record_context {
int timeout; int timeout;
bool show_keycodes; bool show_keycodes;
@ -1208,6 +1216,55 @@ print_libinput_event(struct record_device *dev, struct libinput_event *e)
} }
} }
static bool
handle_hidraw(struct hidraw *hidraw)
{
struct record_device *d = hidraw->device;
unsigned char report[4096];
const char *sep = "";
struct timespec ts;
struct timeval tv;
uint64_t time;
int rc = read(hidraw->fd, report, sizeof(report));
if (rc <= 0)
return false;
/* hidraw doesn't give us a timestamps, we have to make them up */
clock_gettime(CLOCK_MONOTONIC, &ts);
time = s2us(ts.tv_sec) + ns2us(ts.tv_nsec);
/* The first evdev event is guaranteed to have an event time earlier
than now, so we don't set the offset here, we rely on the evdev
events to do so. This potentially leaves us with multiple hidraw
events at timestap 0 but it's too niche to worry about. */
if (d->ctx->offset == 0)
time = 0;
else
time = time_offset(d->ctx, time);
tv = us2tv(time);
iprintf(d->fp, I_EVENTTYPE, "- hid:\n");
iprintf(d->fp, I_EVENT, "time: [%3lu, %6lu]\n", tv.tv_sec, tv.tv_usec);
iprintf(d->fp, I_EVENT, "%s: [", hidraw->name);
for (int byte = 0; byte < rc; byte++) {
if (byte % 16 == 0) {
iprintf(d->fp, I_NONE, "%s\n", sep);
iprintf(d->fp, I_EVENT, " ");
iprintf(d->fp, I_NONE, "0x%02x", report[byte]);
} else {
iprintf(d->fp, I_NONE, "%s0x%02x", sep, report[byte]);
}
sep = ", ";
}
iprintf(d->fp, I_NONE, "\n");
iprintf(d->fp, I_EVENT, "]\n");
return true;
}
static bool static bool
handle_libinput_events(struct record_context *ctx, handle_libinput_events(struct record_context *ctx,
struct record_device *d, struct record_device *d,
@ -2087,6 +2144,16 @@ libinput_ctx_dispatch(struct record_context *ctx, int fd, void *data)
handle_libinput_events(ctx, ctx->first_device, true); handle_libinput_events(ctx, ctx->first_device, true);
} }
static void
hidraw_dispatch(struct record_context *ctx, int fd, void *data)
{
struct hidraw *hidraw = data;
ctx->had_events = true;
ctx->timestamps.had_events_since_last_time = true;
handle_hidraw(hidraw);
}
static int static int
dispatch_sources(struct record_context *ctx) dispatch_sources(struct record_context *ctx)
{ {
@ -2136,7 +2203,13 @@ mainloop(struct record_context *ctx)
arm_timer(timerfd); arm_timer(timerfd);
list_for_each(d, &ctx->devices, link) { list_for_each(d, &ctx->devices, link) {
struct hidraw *hidraw;
add_source(ctx, libevdev_get_fd(d->evdev), evdev_dispatch, d); add_source(ctx, libevdev_get_fd(d->evdev), evdev_dispatch, d);
list_for_each(hidraw, &d->hidraw_devices, link) {
add_source(ctx, hidraw->fd, hidraw_dispatch, hidraw);
}
} }
if (ctx->libinput) { if (ctx->libinput) {
@ -2291,6 +2364,8 @@ init_device(struct record_context *ctx, const char *path, bool grab)
d->ctx = ctx; d->ctx = ctx;
d->devnode = safe_strdup(path); d->devnode = safe_strdup(path);
list_init(&d->hidraw_devices);
fd = open(d->devnode, O_RDONLY|O_NONBLOCK); fd = open(d->devnode, O_RDONLY|O_NONBLOCK);
if (fd < 0) { if (fd < 0) {
fprintf(stderr, fprintf(stderr,
@ -2389,6 +2464,52 @@ init_libinput(struct record_context *ctx)
return true; return true;
} }
static bool
init_hidraw(struct record_context *ctx)
{
struct record_device *dev;
list_for_each(dev, &ctx->devices, link) {
char syspath[PATH_MAX];
DIR *dir;
struct dirent *entry;
snprintf(syspath,
sizeof(syspath),
"/sys/class/input/%s/device/device/hidraw",
safe_basename(dev->devnode));
dir = opendir(syspath);
if (!dir)
continue;
while ((entry = readdir(dir))) {
char hidraw_node[PATH_MAX];
int fd;
struct hidraw *hidraw = NULL;
if (!strstartswith(entry->d_name, "hidraw"))
continue;
snprintf(hidraw_node,
sizeof(hidraw_node),
"/dev/%s",
entry->d_name);
fd = open(hidraw_node, O_RDONLY|O_NONBLOCK);
if (fd == -1)
continue;
hidraw = zalloc(sizeof(*hidraw));
hidraw->fd = fd;
hidraw->name = strdup(entry->d_name);
hidraw->device = dev;
list_insert(&dev->hidraw_devices, &hidraw->link);
}
closedir(dir);
}
return true;
}
static void static void
usage(void) usage(void)
{ {
@ -2506,6 +2627,7 @@ enum options {
OPT_MULTIPLE, OPT_MULTIPLE,
OPT_ALL, OPT_ALL,
OPT_LIBINPUT, OPT_LIBINPUT,
OPT_HIDRAW,
OPT_GRAB, OPT_GRAB,
}; };
@ -2524,12 +2646,16 @@ main(int argc, char **argv)
{ "all", no_argument, 0, OPT_ALL }, { "all", no_argument, 0, OPT_ALL },
{ "help", no_argument, 0, OPT_HELP }, { "help", no_argument, 0, OPT_HELP },
{ "with-libinput", no_argument, 0, OPT_LIBINPUT }, { "with-libinput", no_argument, 0, OPT_LIBINPUT },
{ "with-hidraw", no_argument, 0, OPT_HIDRAW },
{ "grab", no_argument, 0, OPT_GRAB }, { "grab", no_argument, 0, OPT_GRAB },
{ 0, 0, 0, 0 }, { 0, 0, 0, 0 },
}; };
struct record_device *d; struct record_device *d;
const char *output_arg = NULL; const char *output_arg = NULL;
bool all = false, with_libinput = false, grab = false; bool all = false,
with_libinput = false,
with_hidraw = false,
grab = false;
int ndevices; int ndevices;
int rc = EXIT_FAILURE; int rc = EXIT_FAILURE;
char **paths = NULL; char **paths = NULL;
@ -2575,6 +2701,10 @@ main(int argc, char **argv)
case OPT_LIBINPUT: case OPT_LIBINPUT:
with_libinput = true; with_libinput = true;
break; break;
case OPT_HIDRAW:
with_hidraw = true;
fprintf(stderr, "# WARNING: do not type passwords while recording HID reports\n");
break;
case OPT_GRAB: case OPT_GRAB:
grab = true; grab = true;
break; break;
@ -2650,10 +2780,22 @@ main(int argc, char **argv)
if (with_libinput && !init_libinput(&ctx)) if (with_libinput && !init_libinput(&ctx))
goto out; goto out;
if (with_hidraw && !init_hidraw(&ctx))
goto out;
rc = mainloop(&ctx); rc = mainloop(&ctx);
out: out:
strv_free(paths); strv_free(paths);
list_for_each_safe(d, &ctx.devices, link) { list_for_each_safe(d, &ctx.devices, link) {
struct hidraw *hidraw;
list_for_each_safe(hidraw, &d->hidraw_devices, link) {
close(hidraw->fd);
list_remove(&hidraw->link);
free(hidraw->name);
free(hidraw);
}
if (d->device) if (d->device)
libinput_device_unref(d->device); libinput_device_unref(d->device);
free(d->devnode); free(d->devnode);

View file

@ -54,7 +54,7 @@ greater than 0.
.PD 1 .PD 1
Specifies the output file to use. If \fB\-\-autorestart\fR is given, Specifies the output file to use. If \fB\-\-autorestart\fR is given,
the filename is used as prefix only. the filename is used as prefix only.
Where \-\-output-file is not given and the first \fBor\fR last argument is Where \-\-output-file is not given and the first \fBor\fR last argument is
not an input device, the first \fBor\fR last argument will be the output not an input device, the first \fBor\fR last argument will be the output
file. file.
.TP 8 .TP 8
@ -72,6 +72,13 @@ Record libinput events alongside device events.
See section See section
.B RECORDING LIBINPUT EVENTS .B RECORDING LIBINPUT EVENTS
for more details. for more details.
.TP 8
.B \-\-with-hidraw
Record hidraw events alongside device events.
.B DO NOT TYPE SENSITIVE DATA.
See
.B RECORDING HID REPORTS
for more details.
.SH RECORDING MULTIPLE DEVICES .SH RECORDING MULTIPLE DEVICES
Sometimes it is necessary to record the events from multiple devices Sometimes it is necessary to record the events from multiple devices
@ -108,6 +115,15 @@ Note that the libinput context created by \fBlibinput\-record\fR does not
affect the running desktop session and does not (can not!) copy any affect the running desktop session and does not (can not!) copy any
configuration options from that session. configuration options from that session.
.SH RECORDING HID REPORTS
When the \fB\-\-with-hidraw\fR commandline option is given,
\fBlibinput\-record\fR searches for the hidraw node(s) of the given devices
and prints any incoming HID reports from those devices.
.PP
HID reports are \fBnot obfuscated\fR and a sufficiently
motivated person could recover the key strokes from the logs. Do not type
passwords while recording HID reports.
.SH FILE FORMAT .SH FILE FORMAT
The output file format is in YAML and intended to be both human-readable and The output file format is in YAML and intended to be both human-readable and
machine-parseable. Below is a short example YAML file, all keys are detailed machine-parseable. Below is a short example YAML file, all keys are detailed
@ -167,6 +183,9 @@ devices:
- ModelAppleTouchpad=1 - ModelAppleTouchpad=1
- AttrSizeHint=32x32 - AttrSizeHint=32x32
events: events:
- hid:
time: [ 0, 0]
hidraw0: [1, 2, 3, 4]
- evdev: - evdev:
- [ 0, 0, 3, 57, 1420] # EV_ABS / ABS_MT_TRACKING_ID 1420 - [ 0, 0, 3, 57, 1420] # EV_ABS / ABS_MT_TRACKING_ID 1420
- [ 0, 0, 3, 53, 1218] # EV_ABS / ABS_MT_POSITION_X 1218 - [ 0, 0, 3, 53, 1218] # EV_ABS / ABS_MT_POSITION_X 1218
@ -290,7 +309,15 @@ Each \fBevdev\fR dictionary contains the contents of a \fBstruct
input_event\fR in decimal format. The last item in the list is always the input_event\fR in decimal format. The last item in the list is always the
\fBSYN_REPORT\fR of this event frame. The next event frame starts a new \fBSYN_REPORT\fR of this event frame. The next event frame starts a new
\fBevdev\fR dictionary entry in the parent \fBevents\fR list. \fBevdev\fR dictionary entry in the parent \fBevents\fR list.
.TP 8
.B { hid: "hidrawX": [ 12, 34, 56 ], ...] }
The \fBhid\fR dictionary contains the hid reports in decimal format, with
the hidraw node as key. The special key \fBtime\fR denotes the current time
when the report was read from the kernel.
.PP
Note that the kernel does not provide timestamps for hidraw events and the
timestamps provided are from \fBclock_gettime(3)\fR. They may be greater
than a subsequent evdev event's timestamp.
.SH NOTES .SH NOTES
.PP .PP
This tool records events from the kernel and is independent of libinput. In This tool records events from the kernel and is independent of libinput. In

View file

@ -309,7 +309,9 @@ def test_libinput_debug_gui_verbose(libinput_debug_gui):
libinput_debug_gui.run_command_success(["--verbose"]) libinput_debug_gui.run_command_success(["--verbose"])
@pytest.mark.parametrize("arg", ["--help", "--show-keycodes", "--with-libinput"]) @pytest.mark.parametrize(
"arg", ["--help", "--show-keycodes", "--with-libinput", "--with-hidraw"]
)
def test_libinput_record_args(libinput_record, arg): def test_libinput_record_args(libinput_record, arg):
libinput_record.run_command_success([arg]) libinput_record.run_command_success([arg])