/* * Copyright © 2018 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libinput-util.h" #include "libinput-version.h" #include "libinput-git-version.h" static const int FILE_VERSION_NUMBER = 1; struct record_device { struct list link; char *devnode; /* device node of the source device */ struct libevdev *evdev; struct input_event *events; size_t nevents; size_t events_sz; }; struct record_context { int timeout; bool show_keycodes; uint64_t offset; struct list devices; int ndevices; char *outfile; /* file name given on cmdline */ char *output_file; /* full file name with suffix */ int out_fd; unsigned int indent; }; static inline bool obfuscate_keycode(struct input_event *ev) { switch (ev->type) { case EV_KEY: if (ev->code >= KEY_ESC && ev->code < KEY_ZENKAKUHANKAKU) { ev->code = KEY_A; return true; } break; case EV_MSC: if (ev->code == MSC_SCAN) { ev->value = 30; /* KEY_A scancode */ return true; } break; } return false; } static inline void indent_push(struct record_context *ctx) { ctx->indent += 2; } static inline void indent_pop(struct record_context *ctx) { assert(ctx->indent >= 2); ctx->indent -= 2; } /** * Indented dprintf, indentation is given as second parameter. */ static inline void iprintf(const struct record_context *ctx, const char *format, ...) { va_list args; char fmt[1024]; static const char space[] = " "; static const size_t len = sizeof(space); unsigned int indent = ctx->indent; int rc; assert(indent < len); assert(strlen(format) > 1); /* Special case: if we're printing a new list item, we want less * indentation because the '- ' takes up one level of indentation * * This is only needed because I don't want to deal with open/close * lists statements. */ if (format[0] == '-') indent -= 2; snprintf(fmt, sizeof(fmt), "%s%s", &space[len - indent - 1], format); va_start(args, format); rc = vdprintf(ctx->out_fd, fmt, args); va_end(args); assert(rc != -1 && (unsigned int)rc > indent); } /** * Normal printf, just wrapped for the context */ static inline void noiprintf(const struct record_context *ctx, const char *format, ...) { va_list args; int rc; va_start(args, format); rc = vdprintf(ctx->out_fd, format, args); va_end(args); assert(rc != -1 && (unsigned int)rc > 0); } static inline void print_evdev_event(struct record_context *ctx, struct input_event *ev) { const char *cname; bool was_modified = false; char desc[1024]; if (ctx->offset == 0) ctx->offset = tv2us(&ev->time); ev->time = us2tv(tv2us(&ev->time) - ctx->offset); /* Don't leak passwords unless the user wants to */ if (!ctx->show_keycodes) was_modified = obfuscate_keycode(ev); cname = libevdev_event_code_get_name(ev->type, ev->code); if (ev->type == EV_SYN && ev->code == SYN_MT_REPORT) { snprintf(desc, sizeof(desc), "++++++++++++ %s (%d) ++++++++++", cname, ev->value); } else if (ev->type == EV_SYN) { static unsigned long last_ms = 0; unsigned long time, dt; time = us2ms(tv2us(&ev->time)); if (last_ms == 0) last_ms = time; dt = time - last_ms; last_ms = time; snprintf(desc, sizeof(desc), "------------ %s (%d) ---------- %+ldms", cname, ev->value, dt); } else { const char *tname = libevdev_event_type_get_name(ev->type); snprintf(desc, sizeof(desc), "%s / %-20s %4d%s", tname, cname, ev->value, was_modified ? " (obfuscated)" : ""); } iprintf(ctx, "- [%3lu, %6u, %3d, %3d, %5d] # %s\n", ev->time.tv_sec, (unsigned int)ev->time.tv_usec, ev->type, ev->code, ev->value, desc); } static inline void print_evdev_events(struct record_context *ctx, struct input_event *e, size_t nevents) { bool have_ev_syn = true; for (size_t i = 0; i < nevents; i++) { if (have_ev_syn) { iprintf(ctx, "- evdev:\n"); indent_push(ctx); have_ev_syn = false; } print_evdev_event(ctx, &e[i]); if (e[i].type == EV_SYN && e[i].code == SYN_REPORT) { have_ev_syn = true; indent_pop(ctx); } } } static inline size_t handle_evdev_frame(struct record_context *ctx, struct record_device *d) { struct libevdev *evdev = d->evdev; struct input_event e; size_t count = 0; while (libevdev_next_event(evdev, LIBEVDEV_READ_FLAG_NORMAL, &e) == LIBEVDEV_READ_STATUS_SUCCESS) { if (d->nevents == d->events_sz) { void *tmp; d->events_sz += 1000; tmp = realloc(d->events, d->events_sz * sizeof(*d->events)); assert(tmp); d->events = tmp; } d->events[d->nevents++] = e; count++; if (e.type == EV_SYN && e.code == SYN_REPORT) break; } return count; } static inline void handle_events(struct record_context *ctx, struct record_device *d, bool print) { while(true) { size_t first_idx = d->nevents; size_t evcount; evcount = handle_evdev_frame(ctx, d); if (evcount == 0) break; if (!print) continue; print_evdev_events(ctx, &d->events[first_idx], evcount); } } static inline void print_libinput_header(struct record_context *ctx) { iprintf(ctx, "libinput:\n"); indent_push(ctx); iprintf(ctx, "version: \"%s\"\n", LIBINPUT_VERSION); iprintf(ctx, "git: \"%s\"\n", LIBINPUT_GIT_VERSION); if (ctx->timeout > 0) iprintf(ctx, "autorestart: %d\n", ctx->timeout); indent_pop(ctx); } static inline void print_system_header(struct record_context *ctx) { struct utsname u; const char *kernel = "unknown"; FILE *dmi; char modalias[2048] = "unknown"; if (uname(&u) != -1) kernel = u.release; dmi = fopen("/sys/class/dmi/id/modalias", "r"); if (dmi) { if (fgets(modalias, sizeof(modalias), dmi)) { modalias[strlen(modalias) - 1] = '\0'; /* linebreak */ } else { sprintf(modalias, "unknown"); } fclose(dmi); } iprintf(ctx, "system:\n"); indent_push(ctx); iprintf(ctx, "kernel: \"%s\"\n", kernel); iprintf(ctx, "dmi: \"%s\"\n", modalias); indent_pop(ctx); } static inline void print_header(struct record_context *ctx) { iprintf(ctx, "version: %d\n", FILE_VERSION_NUMBER); iprintf(ctx, "ndevices: %d\n", ctx->ndevices); print_libinput_header(ctx); print_system_header(ctx); } static inline void print_description_abs(struct record_context *ctx, struct libevdev *dev, unsigned int code) { const struct input_absinfo *abs; abs = libevdev_get_abs_info(dev, code); assert(abs); iprintf(ctx, "# Value %6d\n", abs->value); iprintf(ctx, "# Min %6d\n", abs->minimum); iprintf(ctx, "# Max %6d\n", abs->maximum); iprintf(ctx, "# Fuzz %6d\n", abs->fuzz); iprintf(ctx, "# Flat %6d\n", abs->flat); iprintf(ctx, "# Resolution %6d\n", abs->resolution); } static inline void print_description_state(struct record_context *ctx, struct libevdev *dev, unsigned int type, unsigned int code) { int state = libevdev_get_event_value(dev, type, code); iprintf(ctx, "# State %d\n", state); } static inline void print_description_codes(struct record_context *ctx, struct libevdev *dev, unsigned int type) { int max; max = libevdev_event_type_get_max(type); if (max == -1) return; iprintf(ctx, "# Event type %d (%s)\n", type, libevdev_event_type_get_name(type)); if (type == EV_SYN) return; for (unsigned int code = 0; code <= (unsigned int)max; code++) { if (!libevdev_has_event_code(dev, type, code)) continue; iprintf(ctx, "# Event code %d (%s)\n", code, libevdev_event_code_get_name(type, code)); switch (type) { case EV_ABS: print_description_abs(ctx, dev, code); break; case EV_LED: case EV_SW: print_description_state(ctx, dev, type, code); break; } } } static inline void print_description(struct record_context *ctx, struct libevdev *dev) { const struct input_absinfo *x, *y; iprintf(ctx, "# Name: %s\n", libevdev_get_name(dev)); iprintf(ctx, "# ID: bus %#02x vendor %#02x product %#02x version %#02x\n", libevdev_get_id_bustype(dev), libevdev_get_id_vendor(dev), libevdev_get_id_product(dev), libevdev_get_id_version(dev)); x = libevdev_get_abs_info(dev, ABS_X); y = libevdev_get_abs_info(dev, ABS_Y); if (x && y) { if (x->resolution && y->resolution) { int w, h; w = (x->maximum - x->minimum)/x->resolution; h = (y->maximum - y->minimum)/y->resolution; iprintf(ctx, "# Size in mm: %dx%d\n", w, h); } else { iprintf(ctx, "# Size in mm: unknown, missing resolution\n"); } } iprintf(ctx, "# Supported Events:\n"); for (unsigned int type = 0; type < EV_CNT; type++) { if (!libevdev_has_event_type(dev, type)) continue; print_description_codes(ctx, dev, type); } iprintf(ctx, "# Properties:\n"); for (unsigned int prop = 0; prop < INPUT_PROP_CNT; prop++) { if (libevdev_has_property(dev, prop)) { iprintf(ctx, "# Property %d (%s)\n", prop, libevdev_property_get_name(prop)); } } } static inline void print_bits_info(struct record_context *ctx, struct libevdev *dev) { iprintf(ctx, "name: \"%s\"\n", libevdev_get_name(dev)); iprintf(ctx, "id: [%d, %d, %d, %d]\n", libevdev_get_id_bustype(dev), libevdev_get_id_vendor(dev), libevdev_get_id_product(dev), libevdev_get_id_version(dev)); } static inline void print_bits_absinfo(struct record_context *ctx, struct libevdev *dev) { const struct input_absinfo *abs; if (!libevdev_has_event_type(dev, EV_ABS)) return; iprintf(ctx, "absinfo:\n"); indent_push(ctx); for (unsigned int code = 0; code < ABS_CNT; code++) { abs = libevdev_get_abs_info(dev, code); if (!abs) continue; iprintf(ctx, "%d: [%d, %d, %d, %d, %d]\n", code, abs->minimum, abs->maximum, abs->fuzz, abs->flat, abs->resolution); } indent_pop(ctx); } static inline void print_bits_codes(struct record_context *ctx, struct libevdev *dev, unsigned int type) { int max; bool first = true; max = libevdev_event_type_get_max(type); if (max == -1) return; iprintf(ctx, "%d: [", type); for (unsigned int code = 0; code <= (unsigned int)max; code++) { if (!libevdev_has_event_code(dev, type, code)) continue; noiprintf(ctx, "%s%d", first ? "" : ", ", code); first = false; } noiprintf(ctx, "] # %s\n", libevdev_event_type_get_name(type)); } static inline void print_bits_types(struct record_context *ctx, struct libevdev *dev) { iprintf(ctx, "codes:\n"); indent_push(ctx); for (unsigned int type = 0; type < EV_CNT; type++) { if (!libevdev_has_event_type(dev, type)) continue; print_bits_codes(ctx, dev, type); } indent_pop(ctx); } static inline void print_bits_props(struct record_context *ctx, struct libevdev *dev) { bool first = true; iprintf(ctx, "properties: ["); for (unsigned int prop = 0; prop < INPUT_PROP_CNT; prop++) { if (libevdev_has_property(dev, prop)) { noiprintf(ctx, "%s%d", first ? "" : ", ", prop); first = false; } } noiprintf(ctx, "]\n"); /* last entry, no comma */ } static inline void print_evdev_description(struct record_context *ctx, struct record_device *dev) { struct libevdev *evdev = dev->evdev; iprintf(ctx, "evdev:\n"); indent_push(ctx); print_description(ctx, evdev); print_bits_info(ctx, evdev); print_bits_types(ctx, evdev); print_bits_absinfo(ctx, evdev); print_bits_props(ctx, evdev); indent_pop(ctx); } static inline void print_udev_properties(struct record_context *ctx, struct record_device *dev) { struct udev *udev = NULL; struct udev_device *udev_device = NULL; struct udev_list_entry *entry; struct stat st; if (stat(dev->devnode, &st) < 0) return; udev = udev_new(); if (!udev) goto out; udev_device = udev_device_new_from_devnum(udev, 'c', st.st_rdev); if (!udev_device) goto out; iprintf(ctx, "udev:\n"); indent_push(ctx); iprintf(ctx, "properties:\n"); indent_push(ctx); entry = udev_device_get_properties_list_entry(udev_device); while (entry) { const char *key, *value; key = udev_list_entry_get_name(entry); if (strneq(key, "ID_INPUT", 8) || strneq(key, "LIBINPUT", 8) || strneq(key, "EV_ABS", 6)) { value = udev_list_entry_get_value(entry); iprintf(ctx, "- %s=%s\n", key, value); } entry = udev_list_entry_get_next(entry); } indent_pop(ctx); indent_pop(ctx); out: udev_device_unref(udev_device); udev_unref(udev); } static inline void print_device_description(struct record_context *ctx, struct record_device *dev) { iprintf(ctx, "- node: %s\n", dev->devnode); print_evdev_description(ctx, dev); print_udev_properties(ctx, dev); } static int is_event_node(const struct dirent *dir) { return strneq(dir->d_name, "event", 5); } static inline char * select_device(void) { struct dirent **namelist; int ndev, selected_device; int rc; char *device_path; ndev = scandir("/dev/input", &namelist, is_event_node, versionsort); if (ndev <= 0) return NULL; fprintf(stderr, "Available devices:\n"); for (int i = 0; i < ndev; i++) { struct libevdev *device; char path[PATH_MAX]; int fd = -1; snprintf(path, sizeof(path), "/dev/input/%s", namelist[i]->d_name); fd = open(path, O_RDONLY); if (fd < 0) continue; rc = libevdev_new_from_fd(fd, &device); close(fd); if (rc != 0) continue; fprintf(stderr, "%s: %s\n", path, libevdev_get_name(device)); libevdev_free(device); } for (int i = 0; i < ndev; i++) free(namelist[i]); free(namelist); fprintf(stderr, "Select the device event number: "); rc = scanf("%d", &selected_device); if (rc != 1 || selected_device < 0) return NULL; rc = xasprintf(&device_path, "/dev/input/event%d", selected_device); if (rc == -1) return NULL; return device_path; } static inline char ** all_devices(void) { struct dirent **namelist; int ndev; int rc; char **devices = NULL; ndev = scandir("/dev/input", &namelist, is_event_node, versionsort); if (ndev <= 0) return NULL; devices = zalloc((ndev + 1)* sizeof *devices); /* NULL-terminated */ for (int i = 0; i < ndev; i++) { char *device_path; rc = xasprintf(&device_path, "/dev/input/%s", namelist[i]->d_name); if (rc == -1) goto error; devices[i] = device_path; } return devices; error: if (devices) strv_free(devices); return NULL; } static char * init_output_file(const char *file, bool is_prefix) { char name[PATH_MAX]; assert(file != NULL); if (is_prefix) { struct tm *tm; time_t t; char suffix[64]; t = time(NULL); tm = localtime(&t); strftime(suffix, sizeof(suffix), "%F-%T", tm); snprintf(name, sizeof(name), "%s.%s", file, suffix); } else { snprintf(name, sizeof(name), "%s", file); } return strdup(name); } static bool open_output_file(struct record_context *ctx, bool is_prefix) { int out_fd; if (ctx->outfile) { char *fname = init_output_file(ctx->outfile, is_prefix); ctx->output_file = fname; out_fd = open(fname, O_WRONLY|O_CREAT|O_TRUNC, 0666); if (out_fd < 0) return false; } else { ctx->output_file = safe_strdup("stdout"); out_fd = STDOUT_FILENO; } ctx->out_fd = out_fd; return true; } static int mainloop(struct record_context *ctx) { bool autorestart = (ctx->timeout > 0); struct pollfd fds[ctx->ndevices + 1]; struct record_device *d = NULL; struct record_device *first_device = NULL; struct timespec ts; sigset_t mask; int idx; assert(ctx->timeout != 0); assert(!list_empty(&ctx->devices)); sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); sigprocmask(SIG_BLOCK, &mask, NULL); fds[0].fd = signalfd(-1, &mask, SFD_NONBLOCK); fds[0].events = POLLIN; fds[0].revents = 0; assert(fds[0].fd != -1); idx = 1; list_for_each(d, &ctx->devices, link) { fds[idx].fd = libevdev_get_fd(d->evdev); fds[idx].events = POLLIN; fds[idx].revents = 0; assert(fds[idx].fd != -1); idx++; } /* If we have more than one device, the time starts at recording * start time. Otherwise, the first event starts the recording time. */ if (ctx->ndevices > 1) { clock_gettime(CLOCK_MONOTONIC, &ts); ctx->offset = s2us(ts.tv_sec) + ns2us(ts.tv_nsec); } do { int rc; bool had_events = false; /* we delete files without events */ if (!open_output_file(ctx, autorestart)) { fprintf(stderr, "Failed to open '%s'\n", ctx->output_file); break; } fprintf(stderr, "recording to '%s'\n", ctx->output_file); print_header(ctx); if (autorestart) iprintf(ctx, "# Autorestart timeout: %d\n", ctx->timeout); iprintf(ctx, "devices:\n"); indent_push(ctx); /* we only print the first device's description, the * rest is assembled after CTRL+C */ first_device = list_first_entry(&ctx->devices, first_device, link); print_device_description(ctx, first_device); iprintf(ctx, "events:\n"); indent_push(ctx); while (true) { rc = poll(fds, ARRAY_LENGTH(fds), ctx->timeout); if (rc == -1) { /* error */ fprintf(stderr, "Error: %m\n"); autorestart = false; break; } else if (rc == 0) { fprintf(stderr, " ... timeout%s\n", had_events ? "" : " (file is empty)"); break; } else if (fds[0].revents != 0) { /* signal */ autorestart = false; break; } else { /* events */ int is_first = true; had_events = true; list_for_each(d, &ctx->devices, link) { handle_events(ctx, d, is_first); is_first = false; } } } indent_pop(ctx); /* events: */ if (autorestart) { noiprintf(ctx, "# Closing after %ds inactivity", ctx->timeout/1000); } /* First device is printed, now append all the data from the * other devices, if any */ list_for_each(d, &ctx->devices, link) { if (d == list_first_entry(&ctx->devices, d, link)) continue; print_device_description(ctx, d); iprintf(ctx, "events:\n"); indent_push(ctx); print_evdev_events(ctx, d->events, d->nevents); indent_pop(ctx); } indent_pop(ctx); /* devices: */ assert(ctx->indent == 0); fsync(ctx->out_fd); /* If we didn't have events, delete the file. */ if (!isatty(ctx->out_fd)) { if (!had_events && ctx->output_file) { fprintf(stderr, "No events recorded, deleting '%s'\n", ctx->output_file); unlink(ctx->output_file); } close(ctx->out_fd); ctx->out_fd = -1; } free(ctx->output_file); ctx->output_file = NULL; } while (autorestart); close(fds[0].fd); sigprocmask(SIG_UNBLOCK, &mask, NULL); return 0; } static inline bool init_device(struct record_context *ctx, char *path) { struct record_device *d; int fd, rc; d = zalloc(sizeof(*d)); d->devnode = path; d->nevents = 0; d->events_sz = 5000; d->events = zalloc(d->events_sz * sizeof(*d->events)); fd = open(d->devnode, O_RDONLY|O_NONBLOCK); if (fd < 0) { fprintf(stderr, "Failed to open device %s (%m)\n", d->devnode); return false; } rc = libevdev_new_from_fd(fd, &d->evdev); if (rc != 0) { fprintf(stderr, "Failed to create context for %s (%s)\n", d->devnode, strerror(-rc)); close(fd); return false; } libevdev_set_clock_id(d->evdev, CLOCK_MONOTONIC); list_insert(&ctx->devices, &d->link); ctx->ndevices++; return true; } static inline void usage(void) { printf("Usage: %s [--help] [--multiple|--all] [--autorestart] [--output-file filename] [/dev/input/event0] [...]\n" "Common use-cases:\n" "\n" " sudo %s -o recording.yml\n" " Then select the device to record and it Ctrl+C to stop.\n" " The recorded data is in recording.yml and can be attached to a bug report.\n" "\n" " sudo %s -o recording.yml --autorestart 2\n" " As above, but restarts after 2s of inactivity on the device.\n" " Note, the output file is only the prefix.\n" "\n" " sudo %s --multiple -o recording.yml /dev/input/event3 /dev/input/event4\n" " Records the two devices into the same recordings file.\n" "\n" "For more information, see the %s(1) man page\n", program_invocation_short_name, program_invocation_short_name, program_invocation_short_name, program_invocation_short_name, program_invocation_short_name); } enum options { OPT_AUTORESTART, OPT_HELP, OPT_OUTFILE, OPT_KEYCODES, OPT_MULTIPLE, OPT_ALL, }; int main(int argc, char **argv) { struct record_context ctx = { .timeout = -1, .show_keycodes = false, }; struct option opts[] = { { "autorestart", required_argument, 0, OPT_AUTORESTART }, { "output-file", required_argument, 0, OPT_OUTFILE }, { "show-keycodes", no_argument, 0, OPT_KEYCODES }, { "multiple", no_argument, 0, OPT_MULTIPLE }, { "all", no_argument, 0, OPT_ALL }, { "help", no_argument, 0, OPT_HELP }, { 0, 0, 0, 0 }, }; struct record_device *d, *tmp; const char *output_arg = NULL; bool multiple = false, all = false; int ndevices; int rc = 1; list_init(&ctx.devices); 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_AUTORESTART: if (!safe_atoi(optarg, &ctx.timeout) || ctx.timeout <= 0) { usage(); goto out; } ctx.timeout = ctx.timeout * 1000; break; case 'o': case OPT_OUTFILE: output_arg = optarg; break; case OPT_KEYCODES: ctx.show_keycodes = true; break; case OPT_MULTIPLE: multiple = true; break; case OPT_ALL: all = true; break; } } if (all && multiple) { fprintf(stderr, "Only one of --multiple and --all allowed.\n"); goto out; } if (ctx.timeout > 0 && output_arg == NULL) { fprintf(stderr, "Option --autorestart requires --output-file\n"); goto out; } ctx.outfile = safe_strdup(output_arg); ndevices = argc - optind; if (multiple) { if (output_arg == NULL) { fprintf(stderr, "Option --multiple requires --output-file\n"); goto out; } if (ndevices <= 1) { fprintf(stderr, "Option --multiple requires all device nodes on the commandline\n"); goto out; } for (int i = ndevices; i > 0; i -= 1) { char *devnode = safe_strdup(argv[optind + i - 1]); if (!init_device(&ctx, devnode)) goto out; } } else if (all) { char **devices; /* NULL-terminated */ char **d; if (output_arg == NULL) { fprintf(stderr, "Option --all requires --output-file\n"); goto out; } devices = all_devices(); d = devices; while (*d) { if (!init_device(&ctx, safe_strdup(*d))) { strv_free(devices); goto out; } d++; } strv_free(devices); } else { char *path; if (ndevices > 1) { fprintf(stderr, "More than one device, do you want --multiple?\n"); goto out; } path = ndevices <= 0 ? select_device() : safe_strdup(argv[optind++]); if (path == NULL) { fprintf(stderr, "Invalid device path\n"); goto out; } if (!init_device(&ctx, path)) goto out; } rc = mainloop(&ctx); out: list_for_each_safe(d, tmp, &ctx.devices, link) { free(d->devnode); libevdev_free(d->evdev); } return rc; }