tools/record: defer file creation until events arrive in autorestart mode

Previously, --autorestart would create a new output file at the start of
each recording cycle. If no events arrived before the timeout, the empty
file was deleted. This created unnecessary filesystem churn during idle
periods.

Instead, wait for device activity before creating the output file so the
only files we create actually have events in them.

Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1484>
This commit is contained in:
Peter Hutterer 2026-05-26 13:29:56 +10:00
parent 073d502c7f
commit 9ac197b540
3 changed files with 80 additions and 38 deletions

View file

@ -297,10 +297,11 @@ the event log is kept as short as possible.
For bugs that are difficult to reproduce use
``libinput record --autorestart=2 --output-file=recording.yml``.
All events will be recorded to a file named
``recording.yml.<current-date-and-time>`` and whenever the device does not
send events for 2 seconds, a new file is created. This helps to keep
individual recordings short.
The tool will wait for device activity before creating an output file.
Once events are received, they are recorded to a file named
``recording.yml.<current-date-and-time>``. Whenever the device does not
send events for 2 seconds, the current file is closed and a new file is
created once the device sends events again.
To use the ``--autorestart`` option correctly:

View file

@ -2148,20 +2148,42 @@ hidraw_dispatch(struct record_context *ctx, int fd, void *data)
}
static int
dispatch_sources(struct record_context *ctx)
wait_for_sources(struct record_context *ctx,
struct epoll_event *ep,
int max_events,
int timeout_ms)
{
struct source *source;
struct epoll_event ep[64];
int i, count;
int timeout = usec_to_millis(ctx->timeout);
count = epoll_wait(ctx->epoll_fd,
ep,
ARRAY_LENGTH(ep),
timeout > 0 ? timeout : -1);
int count = epoll_wait(ctx->epoll_fd, ep, max_events, timeout_ms);
if (count < 0)
return -errno;
return count;
}
static void
dispatch_ready_sources(struct record_context *ctx, struct epoll_event *ep, int count)
{
for (int i = 0; i < count; ++i) {
struct source *source = ep[i].data.ptr;
if (source->fd == -1)
continue;
source->dispatch(ctx, source->fd, source->user_data);
}
}
static int
dispatch_sources(struct record_context *ctx)
{
struct epoll_event ep[64];
int timeout = usec_to_millis(ctx->timeout);
int count =
wait_for_sources(ctx, ep, ARRAY_LENGTH(ep), timeout > 0 ? timeout : -1);
if (count < 0)
return count;
if (count > 0) {
usec_t now = usec_from_now();
usec_t dt = usec_delta(now, ctx->timestamps.last_wall_time);
@ -2171,13 +2193,7 @@ dispatch_sources(struct record_context *ctx)
}
}
for (i = 0; i < count; ++i) {
source = ep[i].data.ptr;
if (source->fd == -1)
continue;
source->dispatch(ctx, source->fd, source->user_data);
}
dispatch_ready_sources(ctx, ep, count);
return count;
}
@ -2235,6 +2251,34 @@ mainloop(struct record_context *ctx)
do {
struct record_device *d;
struct epoll_event ep[64] = { 0 };
int count = 0;
if (autorestart) {
fprintf(stderr,
"%sWaiting for events...\n",
isatty(STDERR_FILENO) ? "" : "# ");
/* Don't create the output file until we have events */
count = wait_for_sources(ctx, ep, ARRAY_LENGTH(ep), -1);
if (count < 0) {
fprintf(stderr, "Error: %s\n", strerror(-count));
ctx->stop = true;
break;
}
bool has_event_source = false;
for (int i = 0; i < count; i++) {
struct source *s = ep[i].data.ptr;
if (s->dispatch != signalfd_dispatch)
has_event_source = true;
}
if (!has_event_source) {
dispatch_ready_sources(ctx, ep, count);
break;
}
}
if (!open_output_files(ctx, autorestart)) {
fprintf(stderr,
"Failed to open '%s'\n",
@ -2257,8 +2301,6 @@ mainloop(struct record_context *ctx)
iprintf(ctx->first_device->fp, I_TOPLEVEL, "devices:\n");
/* we only print the first device's description, the
* rest is assembled after CTRL+C */
list_for_each(d, &ctx->devices, link) {
print_device_description(d);
iprintf(d->fp, I_DEVICE, "events:\n");
@ -2272,6 +2314,15 @@ mainloop(struct record_context *ctx)
handle_libinput_events(ctx, ctx->first_device, true);
}
if (autorestart) {
/* Dispatch the events that woke us up from
* the idle wait */
dispatch_ready_sources(ctx, ep, count);
if (ctx->first_device->fp != stdout)
print_progress_bar();
}
while (true) {
int rc = dispatch_sources(ctx);
if (rc < 0) { /* error */
@ -2285,9 +2336,7 @@ mainloop(struct record_context *ctx)
break;
if (rc == 0) {
fprintf(stderr,
" ... timeout%s\n",
ctx->had_events ? "" : " (file is empty)");
fprintf(stderr, " ... timeout\n");
break;
}
@ -2325,17 +2374,7 @@ mainloop(struct record_context *ctx)
d->fp = NULL;
}
/* If we didn't have events, delete the file. */
if (!isatty(fileno(ctx->first_device->fp))) {
struct record_device *d;
if (!ctx->had_events && ctx->output_file.name_with_suffix) {
fprintf(stderr,
"No events recorded, deleting '%s'\n",
ctx->output_file.name_with_suffix);
unlink(ctx->output_file.name_with_suffix);
}
list_for_each(d, &ctx->devices, link) {
if (d->fp && d->fp != stdout) {
fclose(d->fp);

View file

@ -42,8 +42,10 @@ nodes may be provided on the commandline.
.B \-\-autorestart=s
Terminate the current recording after
.I s
seconds of device inactivity. If
\fB\-\-output-file\fR is not specified, it defaults to
seconds of device inactivity. The output file is not created until
the first event is received. This avoids creating empty files during
idle periods.
If \fB\-\-output-file\fR is not specified, it defaults to
\fBlibinput-recording.yml\fR. The output filename is used as prefix,
suffixed with the date and time of the recording. The timeout must be
greater than 0.