From 469fb0082190294018112c84fd7973d69437d2c2 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sun, 16 Aug 2026 14:10:42 +0900 Subject: [PATCH 1/3] Add netlink uevent monitor and hotplug diagnostics There's no libudev in the webOS app jail, so the joystick and hidapi backends fall back to rescanning /dev every 3 seconds and diffing a presence bitmask. That's slow to react, and it cannot see a controller that disconnects and reconnects on the same index between two scans, because the bitmask comes out unchanged. Bind a NETLINK_KOBJECT_UEVENT socket instead. The kernel broadcasts add/remove to group 1 with no privilege and no libudev, events are explicit and ordered rather than inferred from a bitmask diff, and they arrive in well under 100ms. This adds the mechanism and a way to measure it. Neither backend is wired up to it yet. Each subsystem is expected to open its own monitor: netlink delivers a copy to every bound socket, whereas sharing one fd would mean whichever side drained it first consumed the other's events. testwebosuevent runs the monitor and the presence poll side by side over the same hotplug activity and reports whether netlink can replace the poll. Unprivileged bind is verified on webOS 10 (kernel 5.4) but not on the 3.10 kernels older versions ship, and that is the open question standing between this and dropping the poll. It reports INCONCLUSIVE rather than a pass when no device came or went, since an idle system produces no uevents whether or not the socket is delivering. That one needs hardware to conclude anything, so the message parsing is covered separately by testwebosuevent_parse, which runs non-interactively under ctest against synthetic uevents. Both follow testevdev: internal sources are compiled in rather than linked, since they're unexported, and SDL_internal.h comes first so the dynapi renaming applies to the public declarations as well as to our calls. Co-Authored-By: Claude Opus 5 (1M context) --- src/joystick/webos/uevent_monitor.c | 244 +++++++++++++++++ src/joystick/webos/uevent_monitor.h | 68 +++++ test/CMakeLists.txt | 10 + test/testwebosuevent.c | 411 ++++++++++++++++++++++++++++ test/testwebosuevent_parse.c | 200 ++++++++++++++ 5 files changed, 933 insertions(+) create mode 100644 src/joystick/webos/uevent_monitor.c create mode 100644 src/joystick/webos/uevent_monitor.h create mode 100644 test/testwebosuevent.c create mode 100644 test/testwebosuevent_parse.c diff --git a/src/joystick/webos/uevent_monitor.c b/src/joystick/webos/uevent_monitor.c new file mode 100644 index 0000000000000..e53790d7daeda --- /dev/null +++ b/src/joystick/webos/uevent_monitor.c @@ -0,0 +1,244 @@ +#include "uevent_monitor.h" + +#include +#include +#include +#include +#include +#include + +#include "SDL_error.h" +#include "SDL_log.h" + +/* Big enough for any single uevent; the kernel caps the payload well below + * this. A short read would only lose the tail of one message, so oversize. */ +#define UEVENT_BUF_SIZE 8192 + +/* Kernel-originated uevents. Group 2 is the udev-processed stream, which + * nothing produces here without udev running. */ +#define UEVENT_GROUP_KERNEL 1 + +struct SDL_webOSUeventMonitor +{ + int fd; + char buf[UEVENT_BUF_SIZE]; +}; + +static int OpenUeventSocket(void); + +static SDL_bool ParseUevent(char *buf, size_t len, SDL_webOSUevent *event); + +static const char *TrailingName(const char *path); + +SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(void) +{ + SDL_webOSUeventMonitor *monitor; + int fd; + + fd = OpenUeventSocket(); + + if (fd < 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, + "Unable to open netlink uevent socket, falling back to polling: %s", + strerror(errno)); + return NULL; + } + + monitor = (SDL_webOSUeventMonitor *)SDL_calloc(1, sizeof(*monitor)); + + if (monitor == NULL) { + close(fd); + SDL_OutOfMemory(); + return NULL; + } + + monitor->fd = fd; + + return monitor; +} + +void SDL_webOSUeventMonitorClose(SDL_webOSUeventMonitor *monitor) +{ + if (monitor == NULL) { + return; + } + + if (monitor->fd >= 0) { + close(monitor->fd); + } + + SDL_free(monitor); +} + +SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUevent *event) +{ + if (monitor == NULL || monitor->fd < 0 || event == NULL) { + return SDL_FALSE; + } + + for (;;) { + struct sockaddr_nl addr; + struct iovec iov; + struct msghdr msg; + ssize_t bytes; + + iov.iov_base = monitor->buf; + /* Leave room to terminate the buffer, so parsing can't run past it + * if the kernel ever hands us an unterminated final field. */ + iov.iov_len = sizeof(monitor->buf) - 1; + + SDL_zero(addr); + SDL_zero(msg); + msg.msg_name = &addr; + msg.msg_namelen = sizeof(addr); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + bytes = recvmsg(monitor->fd, &msg, MSG_DONTWAIT); + + if (bytes <= 0) { + if (bytes < 0 && errno == EINTR) { + continue; + } + /* EAGAIN/EWOULDBLOCK: drained, which is the usual way out. */ + return SDL_FALSE; + } + + /* Only the kernel may hotplug devices. Any other process can bind a + * netlink socket and send us a unicast message, so drop anything + * that isn't from portid 0 and addressed to a multicast group. */ + if (msg.msg_namelen != sizeof(addr) || addr.nl_pid != 0 || addr.nl_groups == 0) { + continue; + } + + monitor->buf[bytes] = '\0'; + + if (ParseUevent(monitor->buf, (size_t)bytes, event)) { + return SDL_TRUE; + } + + /* Not an event we can describe; keep draining rather than making the + * caller poll again for it. */ + } +} + +static int OpenUeventSocket(void) +{ + struct sockaddr_nl addr; + int fd; + int rcvbuf = 1024 * 1024; + + fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); + + if (fd < 0 && (errno == EINVAL || errno == EPROTONOSUPPORT)) { + /* Older kernels reject the socket type flags; set them separately. */ + fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); + + if (fd >= 0) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0 || + fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { + close(fd); + return -1; + } + } + } + + if (fd < 0) { + return -1; + } + + /* A burst of uevents (a hub with several interfaces) can outrun us + * between detect ticks, and an overflowing netlink socket drops + * messages silently. Best effort; the default is workable. */ + setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + + SDL_zero(addr); + addr.nl_family = AF_NETLINK; + addr.nl_groups = UEVENT_GROUP_KERNEL; + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + int saved_errno = errno; + close(fd); + errno = saved_errno; + return -1; + } + + return fd; +} + +/* A kernel uevent is "ACTION@DEVPATH" followed by NUL-separated KEY=VALUE + * fields, e.g. + * + * "add@/devices/.../input/input21/event14\0" + * "ACTION=add\0DEVPATH=/devices/.../event14\0SUBSYSTEM=input\0" + * "DEVNAME=input/event14\0MAJOR=13\0MINOR=78\0..." + * + * Returns SDL_FALSE for anything we can't turn into an add/remove, which + * includes the "change"/"bind"/"move" actions we have no use for. */ +static SDL_bool ParseUevent(char *buf, size_t len, SDL_webOSUevent *event) +{ + const char *devname = NULL; + size_t pos; + + /* libudev's own broadcasts carry a magic prefix instead of the header + * line above. We bind the kernel group so they shouldn't reach us. */ + if (len >= 8 && SDL_memcmp(buf, "libudev", 8) == 0) { + return SDL_FALSE; + } + + SDL_zerop(event); + + /* Skip the header line; ACTION= and DEVPATH= repeat it as proper fields. */ + pos = SDL_strlen(buf) + 1; + + while (pos < len) { + const char *field = &buf[pos]; + size_t field_len = SDL_strlen(field); + + if (SDL_strncmp(field, "ACTION=", 7) == 0) { + const char *action = field + 7; + if (SDL_strcmp(action, "add") == 0) { + event->action = SDL_WEBOS_UEVENT_ACTION_ADD; + } else if (SDL_strcmp(action, "remove") == 0) { + event->action = SDL_WEBOS_UEVENT_ACTION_REMOVE; + } else { + return SDL_FALSE; + } + } else if (SDL_strncmp(field, "SUBSYSTEM=", 10) == 0) { + event->subsystem = field + 10; + } else if (SDL_strncmp(field, "DEVPATH=", 8) == 0) { + event->devpath = field + 8; + } else if (SDL_strncmp(field, "DEVNAME=", 8) == 0) { + /* Present whenever the event describes an actual device node, + * and more trustworthy than the sysfs path, which for some + * subsystems ends in the parent rather than the node. */ + devname = field + 8; + } + + pos += field_len + 1; + } + + if (event->action == SDL_WEBOS_UEVENT_ACTION_OTHER) { + return SDL_FALSE; + } + + /* DEVNAME arrives relative to /dev and may be nested ("input/event14"), + * so reduce either source to the trailing component. A remove event on + * an older kernel can omit DEVNAME, hence the DEVPATH fallback. */ + if (devname != NULL) { + event->devname = TrailingName(devname); + } else if (event->devpath != NULL) { + event->devname = TrailingName(event->devpath); + } + + return SDL_TRUE; +} + +static const char *TrailingName(const char *path) +{ + const char *slash = SDL_strrchr(path, '/'); + const char *name = slash != NULL ? slash + 1 : path; + + return *name != '\0' ? name : NULL; +} diff --git a/src/joystick/webos/uevent_monitor.h b/src/joystick/webos/uevent_monitor.h new file mode 100644 index 0000000000000..1ba83ce32182c --- /dev/null +++ b/src/joystick/webos/uevent_monitor.h @@ -0,0 +1,68 @@ +#include "../../SDL_internal.h" + +#ifndef SDL_webos_uevent_monitor_h_ +#define SDL_webos_uevent_monitor_h_ + +/* Hotplug notifications straight from the kernel, via a + * NETLINK_KOBJECT_UEVENT socket. + * + * There's no libudev in the webOS app jail, so the joystick and hidapi + * backends otherwise fall back to rescanning /dev every 3 seconds and + * diffing a presence bitmask. That's slow to react, and it can't see a + * device that disconnects and reconnects on the same index between two + * scans, because the bitmask comes out unchanged. + * + * The kernel broadcasts add/remove as uevents to anyone bound to group 1, + * with no privilege and no libudev needed. Events are explicit and ordered, + * so the same-index case stops being invisible, and they arrive in well + * under 100ms. + * + * Each subsystem should open its own monitor. Netlink broadcasts a copy to + * every bound socket, so two monitors don't compete; sharing one would mean + * whichever side drained it first consumed the other's events. + */ + +typedef enum SDL_webOSUeventAction +{ + SDL_WEBOS_UEVENT_ACTION_OTHER, + SDL_WEBOS_UEVENT_ACTION_ADD, + SDL_WEBOS_UEVENT_ACTION_REMOVE, +} SDL_webOSUeventAction; + +typedef struct SDL_webOSUeventMonitor SDL_webOSUeventMonitor; + +typedef struct SDL_webOSUevent +{ + SDL_webOSUeventAction action; + + /* "input", "hidraw", ... NULL if the event didn't carry a SUBSYSTEM. */ + const char *subsystem; + + /* Trailing name of the device node, e.g. "event14", "js7", "hidraw0". + * NULL for events that don't describe a node (bus/class-level events). + * Prefix with the right directory to get a path; don't infer ordering + * from it, and key on the name itself. */ + const char *devname; + + /* Full DEVPATH under /sys, for logging. NULL if absent. */ + const char *devpath; +} SDL_webOSUevent; + +/* Returns NULL if the socket can't be created or bound, which the caller + * must treat as a normal outcome and handle by keeping the presence-flag + * poll. Unprivileged bind is the long-standing default and is verified + * working on webOS 10 (kernel 5.4), but it has not been confirmed on the + * 3.10 kernels that older webOS versions ship. */ +extern SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(void); + +extern void SDL_webOSUeventMonitorClose(SDL_webOSUeventMonitor *monitor); + +/* Reads one pending event, without blocking. Returns SDL_FALSE once the + * socket is drained; call it in a loop, since the socket buffer is finite + * and a burst can queue several events. + * + * The strings in `event` point into storage owned by the monitor and stay + * valid only until the next call on the same monitor. */ +extern SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUevent *event); + +#endif /* SDL_webos_uevent_monitor_h_ */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6388f9c0fbc19..9a4f44ccf0a6f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -175,6 +175,16 @@ add_sdl_test_executable(testiconv NEEDS_RESOURCES testiconv.c testutils.c) add_sdl_test_executable(testime NEEDS_RESOURCES testime.c testutils.c) add_sdl_test_executable(testjoystick testjoystick.c) add_sdl_test_executable(testkeys testkeys.c) + +# Netlink uevent hotplug monitor. Not gated on WEBOS: the mechanism is plain +# Linux netlink, and running it on a desktop with a USB controller is useful +# well before it reaches a TV. testwebosuevent needs real hotplug activity to +# conclude anything, so only the parser tests are non-interactive. +if(LINUX) + add_sdl_test_executable(testwebosuevent testwebosuevent.c) + add_sdl_test_executable(testwebosuevent_parse NONINTERACTIVE testwebosuevent_parse.c) +endif() + add_sdl_test_executable(testloadso testloadso.c) add_sdl_test_executable(testlocale NONINTERACTIVE testlocale.c) add_sdl_test_executable(testlock testlock.c) diff --git a/test/testwebosuevent.c b/test/testwebosuevent.c new file mode 100644 index 0000000000000..ea48bce600d12 --- /dev/null +++ b/test/testwebosuevent.c @@ -0,0 +1,411 @@ +/* + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ + +/* Headless diagnostic for the netlink uevent hotplug monitor. + * + * Runs the netlink monitor and the /dev presence poll side by side over the + * same hotplug activity, and reports whether netlink can replace the poll on + * this device. No video, no window: it's meant to be run over SSH on a TV. + * + * The question it exists to answer is whether an unprivileged process can + * receive kernel uevents on a given webOS version. That's confirmed on + * webOS 10 (kernel 5.4) but not on the 3.10 kernels older versions ship, and + * it's the one thing standing between the netlink monitor and dropping the + * 3-second poll. + * + * Note that it can only answer the question if devices actually come and go + * while it runs, which is why it reports INCONCLUSIVE rather than a pass when + * nothing happened. Silence is not evidence that netlink works: an idle + * system produces no uevents whether or not the socket is delivering them. + * + * Usage: + * testwebosuevent [--duration SECONDS] [--poll-interval MS] [--verbose] + * + * Plug and unplug a controller (USB or Bluetooth) a few times while it runs. + * Reconnecting the same controller repeatedly is the interesting case, since + * that's what the presence bitmask can't see. + * + * Exit status: 0 netlink usable, 1 fallback required, 2 inconclusive. + */ + +/* SDL_internal.h must come first, as in testevdev.c: it installs the dynapi + * renaming, so the public declarations that follow get renamed along with our + * calls. Including SDL.h ahead of it leaves calls pointing at SDL_*_REAL with + * no declaration in scope. */ +#include "../src/SDL_internal.h" + +#include +#include +#include +#include + +#include "SDL.h" + +/* Compiled in rather than linked, since these are internal and unexported. */ +#include "../src/joystick/webos/dev_presence.c" +#include "../src/joystick/webos/dev_presence.h" +#include "../src/joystick/webos/uevent_monitor.c" +#include "../src/joystick/webos/uevent_monitor.h" + +/* The presence bitmask is 32 bits wide, so it structurally cannot represent a + * device above this index. Netlink has no such limit, and reporting when we + * cross it is worth doing. */ +#define PRESENCE_MAX_INDEX 32 + +#define TICK_INTERVAL_MS 20 + +typedef enum +{ + NODE_OTHER = -1, + NODE_EVDEV = 0, + NODE_JS, + NODE_HIDRAW, + NODE_KIND_COUNT +} NodeKind; + +typedef struct +{ + const char *label; + const char *prefix; + SDL_webOSDevicePresenceCheck check; + + Uint32 poll_flags; + + /* Indices netlink reported on since the last poll tick, and when it first + * reported each, so we can tell what the poll would have missed and how + * far ahead netlink was. */ + Uint32 netlink_touched; + Uint32 netlink_time[PRESENCE_MAX_INDEX]; +} NodeClass; + +static NodeClass node_classes[NODE_KIND_COUNT] = { + { "evdev", "event", SDL_WEBOS_DEVICE_PRESENCE_CHECK_EVDEV, 0, 0, { 0 } }, + { "js", "js", SDL_WEBOS_DEVICE_PRESENCE_CHECK_JS, 0, 0, { 0 } }, + { "hidraw", "hidraw", SDL_WEBOS_DEVICE_PRESENCE_CHECK_HIDRAW, 0, 0, { 0 } }, +}; + +/* Verdict inputs */ +static int poll_changes = 0; /* bitmask transitions the poll saw */ +static int poll_changes_missed = 0; /* ... that netlink never reported */ +static int netlink_events = 0; /* add/remove on a device node */ +static int netlink_invisible = 0; /* ... that left the bitmask unchanged */ +static int beyond_bitmask = 0; /* nodes the 32-bit mask can't represent */ +static Uint32 latency_total = 0; /* how far netlink led the poll, summed */ +static int latency_samples = 0; + +static int verbose = 0; +static volatile int keep_running = 1; +static Uint32 start_time; + +static void OnSignal(int sig) +{ + (void)sig; + keep_running = 0; +} + +static Uint32 Elapsed(void) +{ + return SDL_GetTicks() - start_time; +} + +static void Report(const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(1); + +static void Report(const char *fmt, ...) +{ + char msg[512]; + va_list ap; + Uint32 ms = Elapsed(); + + va_start(ap, fmt); + SDL_vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + + /* Straight to stdout rather than SDL_Log, so the output stays greppable + * when this is piped over SSH. */ + printf("[%3u.%03u] %s\n", ms / 1000, ms % 1000, msg); + fflush(stdout); +} + +/* Splits "event14" into its class and index. Returns NODE_OTHER for anything + * that isn't a numbered node we track, including "mice" and "mouse0". */ +static NodeKind ClassifyNode(const char *devname, int *index) +{ + int kind; + + if (devname == NULL) { + return NODE_OTHER; + } + + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + const char *prefix = node_classes[kind].prefix; + size_t prefix_len = SDL_strlen(prefix); + const char *suffix; + char *endptr = NULL; + long value; + + if (SDL_strncmp(devname, prefix, prefix_len) != 0) { + continue; + } + + suffix = devname + prefix_len; + + if (*suffix == '\0') { + continue; + } + + value = SDL_strtol(suffix, &endptr, 10); + + if (endptr == NULL || *endptr != '\0' || value < 0) { + continue; + } + + /* "js" is a prefix of nothing else here, but "event" vs "mouse" and + * friends means we only get here on an exact prefix + digits match. */ + *index = (int)value; + return (NodeKind)kind; + } + + return NODE_OTHER; +} + +static void HandleUevent(const SDL_webOSUevent *event) +{ + NodeKind kind; + NodeClass *cls; + int index = 0; + + kind = ClassifyNode(event->devname, &index); + + if (kind == NODE_OTHER) { + if (verbose) { + Report("netlink %-6s %-8s %s (ignored)", + event->action == SDL_WEBOS_UEVENT_ACTION_ADD ? "add" : "remove", + event->subsystem ? event->subsystem : "-", + event->devname ? event->devname : "-"); + } + return; + } + + cls = &node_classes[kind]; + netlink_events++; + + Report("netlink %-6s %s/%d", + event->action == SDL_WEBOS_UEVENT_ACTION_ADD ? "add" : "remove", + cls->label, index); + + if (index >= PRESENCE_MAX_INDEX) { + /* The poll cannot see this device at all, on any timescale. */ + beyond_bitmask++; + Report(" ^ index %d is outside the 32-bit presence bitmask; polling can never see it", index); + return; + } + + if (!(cls->netlink_touched & (1u << index))) { + cls->netlink_time[index] = SDL_GetTicks(); + } + + cls->netlink_touched |= 1u << index; +} + +static void PollPresence(void) +{ + int kind; + + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + NodeClass *cls = &node_classes[kind]; + Uint32 flags = SDL_webOSGetDevicePresenceFlags(cls->check); + Uint32 changed = flags ^ cls->poll_flags; + Uint32 unexplained; + int index; + + for (index = 0; index < PRESENCE_MAX_INDEX; index++) { + Uint32 bit = 1u << index; + + if (!(changed & bit)) { + continue; + } + + poll_changes++; + + if (cls->netlink_touched & bit) { + Uint32 lead = SDL_GetTicks() - cls->netlink_time[index]; + latency_total += lead; + latency_samples++; + Report("poll %-6s %s/%d (netlink was %ums ahead)", + (flags & bit) ? "add" : "remove", cls->label, index, lead); + } else { + poll_changes_missed++; + Report("poll %-6s %s/%d *** netlink did not report this ***", + (flags & bit) ? "add" : "remove", cls->label, index); + } + } + + /* Netlink activity that left the bitmask untouched. A same-index + * disconnect/reconnect between two polls lands here, and it's exactly + * what the poll can never recover. */ + unexplained = cls->netlink_touched & ~changed; + + for (index = 0; index < PRESENCE_MAX_INDEX; index++) { + if (unexplained & (1u << index)) { + netlink_invisible++; + Report(" %s/%d changed but the bitmask did not — invisible to polling", + cls->label, index); + } + } + + cls->poll_flags = flags; + cls->netlink_touched = 0; + } +} + +static int PrintVerdict(SDL_bool monitor_opened) +{ + printf("\n"); + printf("=========================================================\n"); + printf(" netlink events on tracked nodes : %d\n", netlink_events); + printf(" poll-observed changes : %d\n", poll_changes); + printf(" corroborated by netlink : %d\n", poll_changes - poll_changes_missed); + printf(" missed by netlink : %d\n", poll_changes_missed); + printf(" changes only netlink saw : %d\n", netlink_invisible); + printf(" nodes beyond the 32-bit mask : %d\n", beyond_bitmask); + + if (latency_samples > 0) { + printf(" mean netlink lead over poll : %ums over %d samples\n", + latency_total / (Uint32)latency_samples, latency_samples); + } + + printf("---------------------------------------------------------\n"); + + if (!monitor_opened) { + printf(" VERDICT: FALLBACK REQUIRED\n"); + printf(" The netlink socket could not be opened or bound, so this\n"); + printf(" webOS version must keep the presence poll.\n"); + printf("=========================================================\n"); + return 1; + } + + if (netlink_events == 0 && poll_changes == 0) { + printf(" VERDICT: INCONCLUSIVE\n"); + printf(" No device appeared or disappeared during the run, so this\n"); + printf(" says nothing about whether netlink delivers. Re-run and\n"); + printf(" plug/unplug a controller while it is running.\n"); + printf("=========================================================\n"); + return 2; + } + + if (poll_changes_missed > 0) { + printf(" VERDICT: FALLBACK REQUIRED\n"); + printf(" The poll saw %d change(s) netlink never reported, so the\n", poll_changes_missed); + printf(" uevent stream is not reaching this process reliably.\n"); + printf("=========================================================\n"); + return 1; + } + + printf(" VERDICT: NETLINK USABLE\n"); + printf(" Every change the poll detected was reported by netlink first.\n"); + + if (netlink_invisible > 0) { + printf(" %d change(s) were visible only to netlink, which is the\n", netlink_invisible); + printf(" same-index reconnect case the bitmask cannot represent.\n"); + } + + printf("=========================================================\n"); + return 0; +} + +int main(int argc, char *argv[]) +{ + SDL_webOSUeventMonitor *monitor; + Uint32 duration_ms = 30000; + Uint32 poll_interval_ms = 3000; + Uint32 last_poll; + int i; + + for (i = 1; i < argc; i++) { + if (SDL_strcmp(argv[i], "--duration") == 0 && i + 1 < argc) { + duration_ms = (Uint32)SDL_atoi(argv[++i]) * 1000; + } else if (SDL_strcmp(argv[i], "--poll-interval") == 0 && i + 1 < argc) { + poll_interval_ms = (Uint32)SDL_atoi(argv[++i]); + } else if (SDL_strcmp(argv[i], "--verbose") == 0) { + verbose = 1; + } else { + fprintf(stderr, "Usage: %s [--duration SECONDS] [--poll-interval MS] [--verbose]\n", argv[0]); + return 3; + } + } + + if (poll_interval_ms == 0) { + fprintf(stderr, "--poll-interval must be greater than zero\n"); + return 3; + } + + signal(SIGINT, OnSignal); + signal(SIGTERM, OnSignal); + + /* No video, no joystick backend: this exercises the mechanism directly, + * so it stays meaningful before the backends are wired up to it. */ + if (SDL_Init(0) < 0) { + fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); + return 3; + } + + SDL_LogSetPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_VERBOSE); + + start_time = SDL_GetTicks(); + + monitor = SDL_webOSUeventMonitorOpen(); + + Report("netlink monitor: %s", monitor ? "bound to the kernel uevent group" : "UNAVAILABLE"); + + /* Seed the poll state before announcing readiness, so devices that were + * already attached don't register as arrivals on the first tick. The real + * backend has to bind the socket before this scan for the same reason: + * anything appearing in the gap would otherwise go unnoticed. */ + PollPresence(); + poll_changes = 0; + poll_changes_missed = 0; + netlink_events = 0; + netlink_invisible = 0; + latency_total = 0; + latency_samples = 0; + + last_poll = SDL_GetTicks(); + + Report("running for %us — plug and unplug a controller now (Ctrl-C to stop early)", + duration_ms / 1000); + + while (keep_running && Elapsed() < duration_ms) { + if (monitor != NULL) { + SDL_webOSUevent event; + + while (SDL_webOSUeventMonitorPoll(monitor, &event)) { + HandleUevent(&event); + } + } + + if (SDL_TICKS_PASSED(SDL_GetTicks(), last_poll + poll_interval_ms)) { + PollPresence(); + last_poll = SDL_GetTicks(); + } + + SDL_Delay(TICK_INTERVAL_MS); + } + + /* A final poll, so a change in the last interval still gets cross-checked + * instead of being dropped on the floor at exit. */ + PollPresence(); + + SDL_webOSUeventMonitorClose(monitor); + SDL_Quit(); + + return PrintVerdict(monitor != NULL); +} diff --git a/test/testwebosuevent_parse.c b/test/testwebosuevent_parse.c new file mode 100644 index 0000000000000..ea906461edaf5 --- /dev/null +++ b/test/testwebosuevent_parse.c @@ -0,0 +1,200 @@ +/* + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ + +/* Unit tests for the netlink uevent parser. + * + * The companion testwebosuevent needs real hardware to say anything, so this + * covers the message parsing on its own, with synthetic uevents captured from + * a DualShock 4 on webOS 10. Runs anywhere, no devices required. + */ + +#include "../src/SDL_internal.h" + +#include +#include + +#include "SDL_stdinc.h" + +/* Compiled in rather than linked: ParseUevent is static, and this follows the + * same approach testevdev.c takes with SDL_evdev_capabilities.c. */ +#include "../src/joystick/webos/uevent_monitor.c" +#include "../src/joystick/webos/uevent_monitor.h" + +static int failures; + +/* Assembles the NUL-separated field list the kernel actually sends. */ +static size_t BuildUevent(char *buf, const char *const *fields, int count) +{ + size_t pos = 0; + int i; + + for (i = 0; i < count; i++) { + size_t len = SDL_strlen(fields[i]); + SDL_memcpy(buf + pos, fields[i], len + 1); + pos += len + 1; + } + + buf[pos] = '\0'; + + return pos; +} + +static void Check(const char *name, int passed, const char *detail) +{ + printf("%-46s %s\n", name, passed ? "PASS" : "FAIL"); + + if (!passed) { + printf("%-46s got: %s\n", "", detail ? detail : "(null)"); + failures++; + } +} + +int main(int argc, char *argv[]) +{ + char buf[8192]; + SDL_webOSUevent ev; + size_t len; + + (void)argc; + (void)argv; + + /* A typical evdev add, as captured on the C5. */ + { + static const char *const fields[] = { + "add@/devices/platform/soc/usb/0003:054C:09CC.0034/input/input21/event14", + "ACTION=add", + "DEVPATH=/devices/platform/soc/usb/0003:054C:09CC.0034/input/input21/event14", + "SUBSYSTEM=input", + "DEVNAME=input/event14", + "MAJOR=13", + "MINOR=78", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("evdev add -> event14", + ParseUevent(buf, len, &ev) && + ev.action == SDL_WEBOS_UEVENT_ACTION_ADD && + SDL_strcmp(ev.subsystem, "input") == 0 && + SDL_strcmp(ev.devname, "event14") == 0, + ev.devname); + } + + /* Remove with no DEVNAME, which older kernels can send: the trailing + * component of DEVPATH has to carry it instead. */ + { + static const char *const fields[] = { + "remove@/devices/platform/soc/usb/input/input20/js7", + "ACTION=remove", + "DEVPATH=/devices/platform/soc/usb/input/input20/js7", + "SUBSYSTEM=input", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("remove without DEVNAME -> DEVPATH fallback", + ParseUevent(buf, len, &ev) && + ev.action == SDL_WEBOS_UEVENT_ACTION_REMOVE && + SDL_strcmp(ev.devname, "js7") == 0, + ev.devname); + } + + /* hidraw, which is the subsystem the HIDAPI side watches. */ + { + static const char *const fields[] = { + "add@/devices/platform/soc/usb/0003:054C:09CC.0034/hidraw/hidraw0", + "ACTION=add", + "DEVPATH=/devices/platform/soc/usb/0003:054C:09CC.0034/hidraw/hidraw0", + "SUBSYSTEM=hidraw", + "DEVNAME=hidraw0", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("hidraw add -> hidraw0", + ParseUevent(buf, len, &ev) && + SDL_strcmp(ev.subsystem, "hidraw") == 0 && + SDL_strcmp(ev.devname, "hidraw0") == 0, + ev.devname); + } + + /* Actions other than add/remove must be dropped rather than misread as + * an arrival, which would re-add a device on every "change". */ + { + static const char *const fields[] = { + "change@/devices/platform/soc/usb/input/input3/event3", + "ACTION=change", + "DEVPATH=/devices/platform/soc/usb/input/input3/event3", + "SUBSYSTEM=input", + "DEVNAME=input/event3", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("change action rejected", !ParseUevent(buf, len, &ev), "accepted"); + } + + /* bind/unbind arrive for these same devices on modern kernels. */ + { + static const char *const fields[] = { + "bind@/devices/platform/soc/usb/input/input3/event3", + "ACTION=bind", + "SUBSYSTEM=input", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("bind action rejected", !ParseUevent(buf, len, &ev), "accepted"); + } + + /* A libudev-format message must not be read as a kernel one. */ + { + SDL_memcpy(buf, "libudev\0", 8); + SDL_memcpy(buf + 8, "ACTION=add\0", 11); + Check("libudev magic rejected", !ParseUevent(buf, 19, &ev), "accepted"); + } + + { + static const char *const fields[] = { + "@/devices/platform/soc/usb/input/input3/event3", + "SUBSYSTEM=input", + "DEVNAME=input/event3", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("missing ACTION rejected", !ParseUevent(buf, len, &ev), "accepted"); + } + + /* Bus- and class-level events are valid but describe no node. There must + * be no devname invented from the trailing slash. */ + { + static const char *const fields[] = { + "add@/devices/platform/soc/usb/input/input21/", + "ACTION=add", + "DEVPATH=/devices/platform/soc/usb/input/input21/", + "SUBSYSTEM=input", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("trailing-slash DEVPATH -> NULL devname", + ParseUevent(buf, len, &ev) && ev.devname == NULL, + ev.devname); + } + + /* Above index 31, which the 32-bit presence bitmask cannot represent at + * all. Netlink has no such limit and must still report it. */ + { + static const char *const fields[] = { + "add@/devices/platform/soc/usb/input/input40/event40", + "ACTION=add", + "DEVPATH=/devices/platform/soc/usb/input/input40/event40", + "SUBSYSTEM=input", + "DEVNAME=input/event40", + }; + len = BuildUevent(buf, fields, SDL_arraysize(fields)); + Check("event40 parsed (beyond bitmask range)", + ParseUevent(buf, len, &ev) && SDL_strcmp(ev.devname, "event40") == 0, + ev.devname); + } + + printf("\n%s\n", failures == 0 ? "ALL PASS" : "FAILURES PRESENT"); + + return failures != 0; +} From 829ba140b3bec353a3b42c9607adfdedfc86c5a6 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sun, 16 Aug 2026 18:37:47 +0900 Subject: [PATCH 2/3] webos: handle uevent socket overflow, and fix the diagnostic's accounting Netlink is lossy under pressure: rather than blocking the sender, the kernel discards broadcasts and reports ENOBUFS once. Poll() treated that exactly like EAGAIN, so a caller could not tell "socket drained" from "the kernel threw events away", and would carry on believing its device list was in sync. Since a webOS app can be backgrounded or suspended -- which is precisely how the buffer fills while controllers come and go -- that would leave the list permanently stale, strictly worse than the polling this replaces, which self-heals within one interval. Report it instead, via SDL_webOSUeventMonitorLostEvents(), so a backend can respond with the same full scan it does at init. Draining stops at the overflow: a rescan supersedes whatever is still queued, and returning avoids spinning if the condition repeats. This also fixes how testwebosuevent counts what the poll misses. It only flagged an index when the bitmask did not change at all, which catches a reconnect (2 transitions, 0 expressible) but not a whole connect/ disconnect cycle landing between two scans (3 transitions, 1 expressible). It now compares transitions seen against transitions the diff could express, per index, so both collapse the same way. On hardware the corrected count matches the raw event difference exactly: 46 netlink transitions against 34 poll changes, 12 reported lost. The seeding scan is quiet now as well. It ran through the reporting path, so every already-attached device printed as an arrival netlink had failed to report -- 21 alarming lines before the run even started. Tests need __WEBOS__ on webOS builds. SDL_dynapi.h keys SDL_DYNAMIC_API off it, and the library gets it from sdl-build-options while test targets do not, so without it the test compiles against SDL_*_REAL while the library defines the plain names and fails to link. Co-Authored-By: Claude Opus 5 (1M context) --- src/joystick/webos/uevent_monitor.c | 28 ++++++++++ src/joystick/webos/uevent_monitor.h | 16 ++++++ test/CMakeLists.txt | 11 ++++ test/testwebosuevent.c | 82 ++++++++++++++++++++--------- 4 files changed, 111 insertions(+), 26 deletions(-) diff --git a/src/joystick/webos/uevent_monitor.c b/src/joystick/webos/uevent_monitor.c index e53790d7daeda..6b5f4e3a46eb6 100644 --- a/src/joystick/webos/uevent_monitor.c +++ b/src/joystick/webos/uevent_monitor.c @@ -21,6 +21,7 @@ struct SDL_webOSUeventMonitor { int fd; + SDL_bool lost_events; char buf[UEVENT_BUF_SIZE]; }; @@ -100,6 +101,19 @@ SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUe if (bytes < 0 && errno == EINTR) { continue; } + + if (bytes < 0 && errno == ENOBUFS) { + /* The kernel discarded broadcasts because our buffer was + * full. Whatever it dropped is unrecoverable, so stop draining + * and let the caller resync -- a rescan supersedes anything + * still queued, and returning here avoids spinning if the + * condition repeats. */ + monitor->lost_events = SDL_TRUE; + SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, + "Dropped uevents (socket buffer overflow), device list needs a rescan"); + return SDL_FALSE; + } + /* EAGAIN/EWOULDBLOCK: drained, which is the usual way out. */ return SDL_FALSE; } @@ -122,6 +136,20 @@ SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUe } } +SDL_bool SDL_webOSUeventMonitorLostEvents(SDL_webOSUeventMonitor *monitor) +{ + SDL_bool lost; + + if (monitor == NULL) { + return SDL_FALSE; + } + + lost = monitor->lost_events; + monitor->lost_events = SDL_FALSE; + + return lost; +} + static int OpenUeventSocket(void) { struct sockaddr_nl addr; diff --git a/src/joystick/webos/uevent_monitor.h b/src/joystick/webos/uevent_monitor.h index 1ba83ce32182c..12a06e0a9c567 100644 --- a/src/joystick/webos/uevent_monitor.h +++ b/src/joystick/webos/uevent_monitor.h @@ -65,4 +65,20 @@ extern void SDL_webOSUeventMonitorClose(SDL_webOSUeventMonitor *monitor); * valid only until the next call on the same monitor. */ extern SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUevent *event); +/* Reports whether the kernel dropped uevents because the socket buffer filled + * up, and clears the condition. + * + * Netlink is lossy under pressure: rather than blocking the sender, the kernel + * discards broadcasts and reports ENOBUFS once. The events are gone for good, + * so the device list can be wrong in a way no later event will correct -- a + * device removed during the gap simply never gets its remove. + * + * That matters here because a webOS app can be backgrounded or suspended, and + * a process that isn't draining the socket while controllers come and go is + * exactly how the buffer fills. Callers must treat a true return as "resync + * now" and redo the full scan they did at init. Ignoring it would leave the + * list permanently stale, which is worse than the polling it replaces, since + * polling recovers on its own within one interval. */ +extern SDL_bool SDL_webOSUeventMonitorLostEvents(SDL_webOSUeventMonitor *monitor); + #endif /* SDL_webos_uevent_monitor_h_ */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9a4f44ccf0a6f..70e2f07e1598b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -183,6 +183,17 @@ add_sdl_test_executable(testkeys testkeys.c) if(LINUX) add_sdl_test_executable(testwebosuevent testwebosuevent.c) add_sdl_test_executable(testwebosuevent_parse NONINTERACTIVE testwebosuevent_parse.c) + + if(WEBOS) + # These compile internal sources in, so they have to agree with the + # library on SDL_DYNAMIC_API. SDL_dynapi.h keys that off __WEBOS__, + # which the library picks up from sdl-build-options and tests + # otherwise don't -- leaving the test calling SDL_*_REAL while the + # library defines the plain names. SDL_dynapi.h rejects setting + # SDL_DYNAMIC_API on the command line, so match the define instead. + target_compile_definitions(testwebosuevent PRIVATE __WEBOS__) + target_compile_definitions(testwebosuevent_parse PRIVATE __WEBOS__) + endif() endif() add_sdl_test_executable(testloadso testloadso.c) diff --git a/test/testwebosuevent.c b/test/testwebosuevent.c index ea48bce600d12..c93f5d1d499cf 100644 --- a/test/testwebosuevent.c +++ b/test/testwebosuevent.c @@ -85,6 +85,12 @@ typedef struct * far ahead netlink was. */ Uint32 netlink_touched; Uint32 netlink_time[PRESENCE_MAX_INDEX]; + + /* How many transitions netlink reported per index since the last poll. A + * bitmask diff can express at most one, so anything above that is a + * change the poll structurally cannot recover -- a reconnect, or a whole + * connect/disconnect cycle, that happened entirely between two scans. */ + Uint8 netlink_transitions[PRESENCE_MAX_INDEX]; } NodeClass; static NodeClass node_classes[NODE_KIND_COUNT] = { @@ -97,8 +103,9 @@ static NodeClass node_classes[NODE_KIND_COUNT] = { static int poll_changes = 0; /* bitmask transitions the poll saw */ static int poll_changes_missed = 0; /* ... that netlink never reported */ static int netlink_events = 0; /* add/remove on a device node */ -static int netlink_invisible = 0; /* ... that left the bitmask unchanged */ +static int netlink_invisible = 0; /* ... the bitmask diff could not express */ static int beyond_bitmask = 0; /* nodes the 32-bit mask can't represent */ +static int netlink_overflows = 0; /* times the kernel dropped queued uevents */ static Uint32 latency_total = 0; /* how far netlink led the poll, summed */ static int latency_samples = 0; @@ -214,9 +221,17 @@ static void HandleUevent(const SDL_webOSUevent *event) } cls->netlink_touched |= 1u << index; + + if (cls->netlink_transitions[index] < 255) { + cls->netlink_transitions[index]++; + } } -static void PollPresence(void) +/* `announce` is false for the initial seeding scan, where every attached + * device shows up as a change against an empty bitmask. Counting or printing + * those would report the entire existing device list as arrivals that netlink + * failed to report. */ +static void PollPresence(SDL_bool announce) { int kind; @@ -224,10 +239,9 @@ static void PollPresence(void) NodeClass *cls = &node_classes[kind]; Uint32 flags = SDL_webOSGetDevicePresenceFlags(cls->check); Uint32 changed = flags ^ cls->poll_flags; - Uint32 unexplained; int index; - for (index = 0; index < PRESENCE_MAX_INDEX; index++) { + for (index = 0; announce && index < PRESENCE_MAX_INDEX; index++) { Uint32 bit = 1u << index; if (!(changed & bit)) { @@ -249,21 +263,25 @@ static void PollPresence(void) } } - /* Netlink activity that left the bitmask untouched. A same-index - * disconnect/reconnect between two polls lands here, and it's exactly - * what the poll can never recover. */ - unexplained = cls->netlink_touched & ~changed; - - for (index = 0; index < PRESENCE_MAX_INDEX; index++) { - if (unexplained & (1u << index)) { - netlink_invisible++; - Report(" %s/%d changed but the bitmask did not — invisible to polling", - cls->label, index); + /* A bitmask diff carries at most one transition per index, so compare + * what netlink reported against what the diff could express. Anything + * over is lost for good: a same-index reconnect (2 transitions, 0 + * expressible) or a full connect/disconnect cycle landing between two + * scans (3 transitions, 1 expressible). */ + for (index = 0; announce && index < PRESENCE_MAX_INDEX; index++) { + int seen = cls->netlink_transitions[index]; + int expressible = (changed & (1u << index)) ? 1 : 0; + + if (seen > expressible) { + netlink_invisible += seen - expressible; + Report(" %s/%d: netlink saw %d transition(s), the bitmask could express %d", + cls->label, index, seen, expressible); } } cls->poll_flags = flags; cls->netlink_touched = 0; + SDL_memset(cls->netlink_transitions, 0, sizeof(cls->netlink_transitions)); } } @@ -275,8 +293,9 @@ static int PrintVerdict(SDL_bool monitor_opened) printf(" poll-observed changes : %d\n", poll_changes); printf(" corroborated by netlink : %d\n", poll_changes - poll_changes_missed); printf(" missed by netlink : %d\n", poll_changes_missed); - printf(" changes only netlink saw : %d\n", netlink_invisible); + printf(" transitions the poll cannot see : %d\n", netlink_invisible); printf(" nodes beyond the 32-bit mask : %d\n", beyond_bitmask); + printf(" socket overflows (events lost) : %d\n", netlink_overflows); if (latency_samples > 0) { printf(" mean netlink lead over poll : %ums over %d samples\n", @@ -314,8 +333,8 @@ static int PrintVerdict(SDL_bool monitor_opened) printf(" Every change the poll detected was reported by netlink first.\n"); if (netlink_invisible > 0) { - printf(" %d change(s) were visible only to netlink, which is the\n", netlink_invisible); - printf(" same-index reconnect case the bitmask cannot represent.\n"); + printf(" netlink reported %d transition(s) more than the bitmask diff\n", netlink_invisible); + printf(" could express -- reconnects the poll structurally cannot see.\n"); } printf("=========================================================\n"); @@ -370,16 +389,18 @@ int main(int argc, char *argv[]) * already attached don't register as arrivals on the first tick. The real * backend has to bind the socket before this scan for the same reason: * anything appearing in the gap would otherwise go unnoticed. */ - PollPresence(); - poll_changes = 0; - poll_changes_missed = 0; - netlink_events = 0; - netlink_invisible = 0; - latency_total = 0; - latency_samples = 0; + PollPresence(SDL_FALSE); last_poll = SDL_GetTicks(); + { + int kind; + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + Report("already attached: %s %s", node_classes[kind].label, + node_classes[kind].poll_flags ? "yes" : "none"); + } + } + Report("running for %us — plug and unplug a controller now (Ctrl-C to stop early)", duration_ms / 1000); @@ -390,10 +411,19 @@ int main(int argc, char *argv[]) while (SDL_webOSUeventMonitorPoll(monitor, &event)) { HandleUevent(&event); } + + /* An overflow means the device list can be stale in a way no + * later event corrects, so a backend has to resync here. Counted + * separately because it invalidates the comparison rather than + * being a netlink failure. */ + if (SDL_webOSUeventMonitorLostEvents(monitor)) { + netlink_overflows++; + Report("netlink *** dropped events (buffer overflow) — a backend must rescan here ***"); + } } if (SDL_TICKS_PASSED(SDL_GetTicks(), last_poll + poll_interval_ms)) { - PollPresence(); + PollPresence(SDL_TRUE); last_poll = SDL_GetTicks(); } @@ -402,7 +432,7 @@ int main(int argc, char *argv[]) /* A final poll, so a change in the last interval still gets cross-checked * instead of being dropped on the floor at exit. */ - PollPresence(); + PollPresence(SDL_TRUE); SDL_webOSUeventMonitorClose(monitor); SDL_Quit(); From f7cb0867c65e7f6f5c33c69f89939ffd4f89c8f6 Mon Sep 17 00:00:00 2001 From: Mariotaku Date: Sun, 16 Aug 2026 18:52:17 +0900 Subject: [PATCH 3/3] webos: replace the 3-second hotplug polls with the uevent monitor Both backends now take hotplug from the netlink stream instead of rescanning /dev on a timer. LINUX_JoystickDetect drains a monitor rather than diffing a presence bitmask, and the hidapi discovery loses its separate 3-second hidraw scan. The monitor is self-contained, so using it is no harder than the poll it replaces: open one for a node class and drain it wherever you used to poll. Open() binds the socket and only then enumerates what's already attached, queueing those as ordinary add events -- callers get one code path for existing and hotplugged devices, and the bind-before-scan ordering is handled here rather than left to each integration to remember. Poll() filters to the requested class, collapses events that would not change what the caller believes, and hands back a devnode ready for MaybeAddDevice(). Overflow recovery moved inside for the same reason. It was exposed as LostEvents(), which only worked if the caller remembered to ask; forget it and the device list goes permanently stale, silently. The monitor now re-enumerates on ENOBUFS and reports the difference against what it last told the caller, so the caller sees a correct add/remove stream either way. Polling stays for the cases that need it: a socket that will not open, and the re-enumeration behind the overflow recovery. Measured inside the app jail across three kernel generations -- webOS 3.4 (3.10.19), 4 (4.4.84) and 10 (5.4.268). Over 111 poll-detected changes netlink missed none, while the poll lost 23 transitions netlink caught, including whole connect/disconnect cycles that landed between two scans and left the bitmask unchanged. testwebosuevent grows a --sdl mode that reports SDL's own joystick device events, which checks the backend wiring rather than the stream underneath it. On a webOS 3.4 TV it shows two removes 35ms apart and several add/remove pairs under a second, all resolved individually -- none of which the 3-second poll could express. Co-Authored-By: Claude Opus 5 (1M context) --- src/hidapi/SDL_hidapi.c | 27 +- src/joystick/linux/SDL_sysjoystick.c | 44 ++- src/joystick/webos/uevent_monitor.c | 389 ++++++++++++++++++++++++--- src/joystick/webos/uevent_monitor.h | 88 +++--- test/testwebosuevent.c | 266 +++++++++--------- test/testwebosuevent_parse.c | 41 ++- 6 files changed, 637 insertions(+), 218 deletions(-) diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c index a2c2a4d9cbca7..9736e8260881a 100644 --- a/src/hidapi/SDL_hidapi.c +++ b/src/hidapi/SDL_hidapi.c @@ -56,6 +56,7 @@ #ifdef __WEBOS__ #include "../joystick/webos/dev_presence.h" +#include "../joystick/webos/uevent_monitor.h" #endif /* __WEBOS__ */ #include "../core/linux/SDL_udev.h" @@ -84,6 +85,7 @@ typedef enum ENUMERATION_FALLBACK, #ifdef __WEBOS__ ENUMERATION_POLLING, + ENUMERATION_NETLINK, #endif } LinuxEnumerationMethod; @@ -125,6 +127,7 @@ static struct #endif #ifdef __WEBOS__ Uint32 m_unPresenceFlags; + SDL_webOSUeventMonitor *m_pUeventMonitor; #endif } SDL_HIDAPI_discovery; @@ -343,7 +346,13 @@ static void HIDAPI_InitializeDiscovery(void) } else #endif /* SDL_USE_LIBUDEV */ #ifdef __WEBOS__ - if (linux_enumeration_method == ENUMERATION_POLLING) { + if (linux_enumeration_method == ENUMERATION_POLLING || linux_enumeration_method == ENUMERATION_NETLINK) { + /* Hidraw hotplug has its own monitor: netlink broadcasts a copy to + * every bound socket, but a single fd shared with the joystick + * backend would mean whichever drained first ate the other's + * events. Without one we keep the 3s presence poll below. */ + SDL_HIDAPI_discovery.m_pUeventMonitor = + SDL_webOSUeventMonitorOpen(SDL_WEBOS_DEVICE_PRESENCE_CHECK_HIDRAW); SDL_HIDAPI_discovery.m_bCanGetNotifications = SDL_TRUE; } else #endif @@ -454,7 +463,16 @@ static void HIDAPI_UpdateDiscovery(void) } else #endif /* SDL_USE_LIBUDEV */ #ifdef __WEBOS__ - if (linux_enumeration_method == ENUMERATION_POLLING) { + if (SDL_HIDAPI_discovery.m_pUeventMonitor != NULL) { + SDL_webOSUevent event; + + /* Every add or remove counts; the enumeration behind + * SDL_hid_device_change_count() re-reads /dev/hidraw* anyway, so the + * node itself doesn't matter here, only that something moved. */ + while (SDL_webOSUeventMonitorPoll(SDL_HIDAPI_discovery.m_pUeventMonitor, &event)) { + ++SDL_HIDAPI_discovery.m_unDeviceChangeCounter; + } + } else if (linux_enumeration_method == ENUMERATION_POLLING) { const Uint32 SDL_HIDAPI_DETECT_INTERVAL_MS = 3000; /* Update every 3 seconds */ Uint32 now = SDL_GetTicks(); Uint32 next_detect = SDL_HIDAPI_discovery.m_unLastDetect + SDL_HIDAPI_DETECT_INTERVAL_MS; @@ -517,6 +535,11 @@ static void HIDAPI_ShutdownDiscovery(void) return; } +#ifdef __WEBOS__ + SDL_webOSUeventMonitorClose(SDL_HIDAPI_discovery.m_pUeventMonitor); + SDL_HIDAPI_discovery.m_pUeventMonitor = NULL; +#endif + #if defined(__WIN32__) || defined(__WINGDK__) if (SDL_HIDAPI_discovery.m_hNotify) { UnregisterDeviceNotification(SDL_HIDAPI_discovery.m_hNotify); diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c index e1f22c7960003..798427d1b36d4 100644 --- a/src/joystick/linux/SDL_sysjoystick.c +++ b/src/joystick/linux/SDL_sysjoystick.c @@ -48,6 +48,9 @@ #include "../../events/SDL_events_c.h" #include "../SDL_sysjoystick.h" #include "../SDL_joystick_c.h" +#ifdef __WEBOS__ +#include "../webos/uevent_monitor.h" +#endif #include "../steam/SDL_steamcontroller.h" #include "SDL_sysjoystick_c.h" #include "../hidapi/SDL_hidapijoystick_c.h" @@ -147,6 +150,7 @@ typedef enum ENUMERATION_FALLBACK, #ifdef __WEBOS__ ENUMERATION_POLLING, + ENUMERATION_NETLINK, #endif } EnumerationMethod; @@ -190,6 +194,9 @@ static SDL_joylist_item *SDL_joylist_tail SDL_GUARDED_BY(SDL_joystick_lock) = NU static int numjoysticks SDL_GUARDED_BY(SDL_joystick_lock) = 0; static SDL_sensorlist_item *SDL_sensorlist SDL_GUARDED_BY(SDL_joystick_lock) = NULL; static int inotify_fd = -1; +#ifdef __WEBOS__ +static SDL_webOSUeventMonitor *joystick_uevent_monitor = NULL; +#endif static Uint32 last_joy_detect_time; static time_t last_input_dir_mtime; @@ -1034,8 +1041,32 @@ static void LINUX_FallbackJoystickDetect(void) } } +#ifdef __WEBOS__ +/* The uevent stream is explicit and ordered, so a device that disconnects and + * reconnects on the same index between two ticks produces a remove and an add + * rather than an unchanged presence bitmask. Devices already attached at init + * arrive here as ordinary adds, so there's no separate startup scan. */ +static void LINUX_NetlinkJoystickDetect(void) +{ + SDL_webOSUevent event; + + while (SDL_webOSUeventMonitorPoll(joystick_uevent_monitor, &event)) { + if (event.action == SDL_WEBOS_UEVENT_ACTION_ADD) { + MaybeAddDevice(event.devnode); + } else { + MaybeRemoveDevice(event.devnode); + } + } +} +#endif + static void LINUX_JoystickDetect(void) { +#ifdef __WEBOS__ + if (enumeration_method == ENUMERATION_NETLINK) { + LINUX_NetlinkJoystickDetect(); + } else +#endif #ifdef SDL_USE_LIBUDEV if (enumeration_method == ENUMERATION_LIBUDEV) { SDL_UDEV_Poll(); @@ -1103,7 +1134,13 @@ static int LINUX_JoystickInit(void) SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "Container detected, disabling udev integration"); #ifdef __WEBOS__ - enumeration_method = ENUMERATION_POLLING; + /* No libudev in the app jail, so hotplug comes from a netlink + * uevent socket. Polling is the fallback for a kernel that won't + * let us bind one. */ + joystick_uevent_monitor = SDL_webOSUeventMonitorOpen( + SDL_classic_joysticks ? SDL_WEBOS_DEVICE_PRESENCE_CHECK_JS + : SDL_WEBOS_DEVICE_PRESENCE_CHECK_EVDEV); + enumeration_method = joystick_uevent_monitor ? ENUMERATION_NETLINK : ENUMERATION_POLLING; #else enumeration_method = ENUMERATION_FALLBACK; #endif @@ -2315,6 +2352,11 @@ static void LINUX_JoystickQuit(void) inotify_fd = -1; } +#ifdef __WEBOS__ + SDL_webOSUeventMonitorClose(joystick_uevent_monitor); + joystick_uevent_monitor = NULL; +#endif + for (item = SDL_joylist; item; item = next) { next = item->next; FreeJoylistItem(item); diff --git a/src/joystick/webos/uevent_monitor.c b/src/joystick/webos/uevent_monitor.c index 6b5f4e3a46eb6..4a10c47c9a08f 100644 --- a/src/joystick/webos/uevent_monitor.c +++ b/src/joystick/webos/uevent_monitor.c @@ -1,10 +1,12 @@ #include "uevent_monitor.h" +#include #include #include #include #include #include +#include #include #include "SDL_error.h" @@ -18,24 +20,90 @@ * nothing produces here without udev running. */ #define UEVENT_GROUP_KERNEL 1 +/* "hidraw" plus digits, with room to spare. Node names are far shorter. */ +#define NODE_NAME_SIZE 32 + +typedef struct +{ + Uint8 action; + char name[NODE_NAME_SIZE]; +} QueuedEvent; + struct SDL_webOSUeventMonitor { int fd; - SDL_bool lost_events; + + const char *base_dir; /* "/dev" or "/dev/input" */ + const char *prefix; /* "hidraw", "event" or "js" */ + + /* Nodes the caller has been told about, so a re-enumeration after a + * dropped-event burst can report the difference rather than repeating + * the whole device list. */ + char (*known)[NODE_NAME_SIZE]; + int known_count; + int known_capacity; + + /* Events produced by enumeration, ahead of anything read from the + * socket. Drained in order, so startup looks like a burst of adds. */ + QueuedEvent *queue; + int queue_head; + int queue_count; + int queue_capacity; + + /* Storage backing the strings handed out by Poll(). */ + char devname[NODE_NAME_SIZE]; + char devnode[NODE_NAME_SIZE + 16]; + char buf[UEVENT_BUF_SIZE]; }; static int OpenUeventSocket(void); -static SDL_bool ParseUevent(char *buf, size_t len, SDL_webOSUevent *event); +static SDL_bool ParseUevent(char *buf, size_t len, const char **subsystem, const char **devname, + SDL_webOSUeventAction *action); static const char *TrailingName(const char *path); -SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(void) +static SDL_bool NodeNameMatches(const SDL_webOSUeventMonitor *monitor, const char *name); + +static SDL_bool IsKnown(const SDL_webOSUeventMonitor *monitor, const char *name); + +static void MarkKnown(SDL_webOSUeventMonitor *monitor, const char *name); + +static void MarkUnknown(SDL_webOSUeventMonitor *monitor, const char *name); + +static SDL_bool QueueEvent(SDL_webOSUeventMonitor *monitor, SDL_webOSUeventAction action, const char *name); + +static void Resynchronize(SDL_webOSUeventMonitor *monitor); + +static void EmitEvent(SDL_webOSUeventMonitor *monitor, SDL_webOSUeventAction action, const char *name, + SDL_webOSUevent *event); + +SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(SDL_webOSDevicePresenceCheck watch) { SDL_webOSUeventMonitor *monitor; + const char *base_dir; + const char *prefix; int fd; + switch (watch) { + case SDL_WEBOS_DEVICE_PRESENCE_CHECK_HIDRAW: + base_dir = "/dev"; + prefix = "hidraw"; + break; + case SDL_WEBOS_DEVICE_PRESENCE_CHECK_EVDEV: + base_dir = "/dev/input"; + prefix = "event"; + break; + case SDL_WEBOS_DEVICE_PRESENCE_CHECK_JS: + base_dir = "/dev/input"; + prefix = "js"; + break; + default: + SDL_SetError("Unknown device class %d", (int)watch); + return NULL; + } + fd = OpenUeventSocket(); if (fd < 0) { @@ -54,6 +122,13 @@ SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(void) } monitor->fd = fd; + monitor->base_dir = base_dir; + monitor->prefix = prefix; + + /* Bound above, enumerated here: anything that appears in between shows up + * as a uevent we've already started listening for, and the duplicate is + * absorbed by the known-node set. The reverse order would lose it. */ + Resynchronize(monitor); return monitor; } @@ -68,6 +143,8 @@ void SDL_webOSUeventMonitorClose(SDL_webOSUeventMonitor *monitor) close(monitor->fd); } + SDL_free(monitor->known); + SDL_free(monitor->queue); SDL_free(monitor); } @@ -81,8 +158,27 @@ SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUe struct sockaddr_nl addr; struct iovec iov; struct msghdr msg; + const char *subsystem = NULL; + const char *devname = NULL; + SDL_webOSUeventAction action; ssize_t bytes; + /* Anything enumeration produced comes first, so a caller draining in + * a loop sees existing devices before live ones. */ + if (monitor->queue_count > 0) { + QueuedEvent *queued = &monitor->queue[monitor->queue_head]; + + monitor->queue_head++; + monitor->queue_count--; + + if (monitor->queue_count == 0) { + monitor->queue_head = 0; + } + + EmitEvent(monitor, (SDL_webOSUeventAction)queued->action, queued->name, event); + return SDL_TRUE; + } + iov.iov_base = monitor->buf; /* Leave room to terminate the buffer, so parsing can't run past it * if the kernel ever hands us an unterminated final field. */ @@ -103,15 +199,16 @@ SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUe } if (bytes < 0 && errno == ENOBUFS) { - /* The kernel discarded broadcasts because our buffer was - * full. Whatever it dropped is unrecoverable, so stop draining - * and let the caller resync -- a rescan supersedes anything - * still queued, and returning here avoids spinning if the - * condition repeats. */ - monitor->lost_events = SDL_TRUE; + /* The kernel discarded broadcasts because our buffer filled + * up. What it dropped is gone, so rebuild from the device + * tree and report the difference; that supersedes anything + * still queued in the socket, and returning to the top of the + * loop hands the caller the resulting events. */ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, - "Dropped uevents (socket buffer overflow), device list needs a rescan"); - return SDL_FALSE; + "Dropped uevents (socket buffer overflow), re-enumerating %s/%s*", + monitor->base_dir, monitor->prefix); + Resynchronize(monitor); + continue; } /* EAGAIN/EWOULDBLOCK: drained, which is the usual way out. */ @@ -127,27 +224,234 @@ SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUe monitor->buf[bytes] = '\0'; - if (ParseUevent(monitor->buf, (size_t)bytes, event)) { - return SDL_TRUE; + if (!ParseUevent(monitor->buf, (size_t)bytes, &subsystem, &devname, &action)) { + continue; + } + + if (devname == NULL || !NodeNameMatches(monitor, devname)) { + continue; + } + + /* Collapse anything that doesn't change what the caller believes. + * The kernel can emit more than one event for a node, and after a + * re-enumeration we may still have the original uevent queued. */ + if (action == SDL_WEBOS_UEVENT_ACTION_ADD) { + if (IsKnown(monitor, devname)) { + continue; + } + MarkKnown(monitor, devname); + } else { + if (!IsKnown(monitor, devname)) { + continue; + } + MarkUnknown(monitor, devname); } - /* Not an event we can describe; keep draining rather than making the - * caller poll again for it. */ + EmitEvent(monitor, action, devname, event); + return SDL_TRUE; } } -SDL_bool SDL_webOSUeventMonitorLostEvents(SDL_webOSUeventMonitor *monitor) +/* Lists the nodes currently present and queues the difference against what + * the caller has already been told, as add/remove events. Used for the + * initial enumeration (where everything is new) and to recover from dropped + * events (where usually nothing is). */ +static void Resynchronize(SDL_webOSUeventMonitor *monitor) { - SDL_bool lost; + char (*present)[NODE_NAME_SIZE] = NULL; + int present_count = 0; + int present_capacity = 0; + DIR *dir; + struct dirent *entry; + int i; - if (monitor == NULL) { + dir = opendir(monitor->base_dir); + + if (dir == NULL) { + return; + } + + while ((entry = readdir(dir)) != NULL) { + struct stat st; + char path[NODE_NAME_SIZE + 16]; + + if (!NodeNameMatches(monitor, entry->d_name)) { + continue; + } + + SDL_snprintf(path, sizeof(path), "%s/%s", monitor->base_dir, entry->d_name); + + if (stat(path, &st) != 0 || !S_ISCHR(st.st_mode)) { + continue; + } + + if (present_count == present_capacity) { + int capacity = present_capacity ? present_capacity * 2 : 8; + void *resized = SDL_realloc(present, (size_t)capacity * NODE_NAME_SIZE); + + if (resized == NULL) { + break; + } + + present = (char(*)[NODE_NAME_SIZE])resized; + present_capacity = capacity; + } + + SDL_strlcpy(present[present_count], entry->d_name, NODE_NAME_SIZE); + present_count++; + } + + closedir(dir); + + /* Gone: known but no longer on disk. Walk backwards, since removing from + * the known set swaps the tail into the current slot. */ + for (i = monitor->known_count - 1; i >= 0; i--) { + SDL_bool still_there = SDL_FALSE; + int j; + + for (j = 0; j < present_count; j++) { + if (SDL_strcmp(monitor->known[i], present[j]) == 0) { + still_there = SDL_TRUE; + break; + } + } + + if (!still_there) { + char name[NODE_NAME_SIZE]; + + SDL_strlcpy(name, monitor->known[i], sizeof(name)); + MarkUnknown(monitor, name); + QueueEvent(monitor, SDL_WEBOS_UEVENT_ACTION_REMOVE, name); + } + } + + /* New: on disk but not yet reported. */ + for (i = 0; i < present_count; i++) { + if (!IsKnown(monitor, present[i])) { + if (QueueEvent(monitor, SDL_WEBOS_UEVENT_ACTION_ADD, present[i])) { + MarkKnown(monitor, present[i]); + } + } + } + + SDL_free(present); +} + +static void EmitEvent(SDL_webOSUeventMonitor *monitor, SDL_webOSUeventAction action, const char *name, + SDL_webOSUevent *event) +{ + SDL_strlcpy(monitor->devname, name, sizeof(monitor->devname)); + SDL_snprintf(monitor->devnode, sizeof(monitor->devnode), "%s/%s", monitor->base_dir, name); + + event->action = action; + event->devname = monitor->devname; + event->devnode = monitor->devnode; +} + +/* True for exactly the monitor's node class: its prefix followed by digits, + * so an "event" monitor doesn't pick up "mice", and a "js" monitor doesn't + * pick up anything else that happens to start with those letters. */ +static SDL_bool NodeNameMatches(const SDL_webOSUeventMonitor *monitor, const char *name) +{ + size_t prefix_len = SDL_strlen(monitor->prefix); + const char *suffix; + + if (SDL_strncmp(name, monitor->prefix, prefix_len) != 0) { + return SDL_FALSE; + } + + suffix = name + prefix_len; + + if (*suffix == '\0' || SDL_strlen(name) >= NODE_NAME_SIZE) { return SDL_FALSE; } - lost = monitor->lost_events; - monitor->lost_events = SDL_FALSE; + for (; *suffix != '\0'; suffix++) { + if (*suffix < '0' || *suffix > '9') { + return SDL_FALSE; + } + } + + return SDL_TRUE; +} + +static SDL_bool IsKnown(const SDL_webOSUeventMonitor *monitor, const char *name) +{ + int i; + + for (i = 0; i < monitor->known_count; i++) { + if (SDL_strcmp(monitor->known[i], name) == 0) { + return SDL_TRUE; + } + } + + return SDL_FALSE; +} + +static void MarkKnown(SDL_webOSUeventMonitor *monitor, const char *name) +{ + if (IsKnown(monitor, name)) { + return; + } + + if (monitor->known_count == monitor->known_capacity) { + int capacity = monitor->known_capacity ? monitor->known_capacity * 2 : 8; + void *resized = SDL_realloc(monitor->known, (size_t)capacity * NODE_NAME_SIZE); + + if (resized == NULL) { + return; + } + + monitor->known = (char(*)[NODE_NAME_SIZE])resized; + monitor->known_capacity = capacity; + } + + SDL_strlcpy(monitor->known[monitor->known_count], name, NODE_NAME_SIZE); + monitor->known_count++; +} - return lost; +static void MarkUnknown(SDL_webOSUeventMonitor *monitor, const char *name) +{ + int i; + + for (i = 0; i < monitor->known_count; i++) { + if (SDL_strcmp(monitor->known[i], name) == 0) { + SDL_strlcpy(monitor->known[i], monitor->known[monitor->known_count - 1], NODE_NAME_SIZE); + monitor->known_count--; + return; + } + } +} + +static SDL_bool QueueEvent(SDL_webOSUeventMonitor *monitor, SDL_webOSUeventAction action, const char *name) +{ + QueuedEvent *slot; + + if (monitor->queue_head + monitor->queue_count == monitor->queue_capacity) { + if (monitor->queue_head > 0) { + /* Reclaim the drained prefix before growing. */ + SDL_memmove(monitor->queue, &monitor->queue[monitor->queue_head], + (size_t)monitor->queue_count * sizeof(*monitor->queue)); + monitor->queue_head = 0; + } else { + int capacity = monitor->queue_capacity ? monitor->queue_capacity * 2 : 8; + void *resized = SDL_realloc(monitor->queue, (size_t)capacity * sizeof(*monitor->queue)); + + if (resized == NULL) { + return SDL_FALSE; + } + + monitor->queue = (QueuedEvent *)resized; + monitor->queue_capacity = capacity; + } + } + + slot = &monitor->queue[monitor->queue_head + monitor->queue_count]; + slot->action = (Uint8)action; + SDL_strlcpy(slot->name, name, sizeof(slot->name)); + monitor->queue_count++; + + return SDL_TRUE; } static int OpenUeventSocket(void) @@ -176,9 +480,9 @@ static int OpenUeventSocket(void) return -1; } - /* A burst of uevents (a hub with several interfaces) can outrun us - * between detect ticks, and an overflowing netlink socket drops - * messages silently. Best effort; the default is workable. */ + /* A burst of uevents (a wireless receiver enumerating several interfaces) + * can outrun us between detect ticks, and an overflowing netlink socket + * drops messages. Best effort; overflow is handled either way. */ setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); SDL_zero(addr); @@ -204,9 +508,12 @@ static int OpenUeventSocket(void) * * Returns SDL_FALSE for anything we can't turn into an add/remove, which * includes the "change"/"bind"/"move" actions we have no use for. */ -static SDL_bool ParseUevent(char *buf, size_t len, SDL_webOSUevent *event) +static SDL_bool ParseUevent(char *buf, size_t len, const char **subsystem, const char **devname, + SDL_webOSUeventAction *action) { - const char *devname = NULL; + const char *devpath = NULL; + const char *node = NULL; + SDL_bool have_action = SDL_FALSE; size_t pos; /* libudev's own broadcasts carry a magic prefix instead of the header @@ -215,7 +522,8 @@ static SDL_bool ParseUevent(char *buf, size_t len, SDL_webOSUevent *event) return SDL_FALSE; } - SDL_zerop(event); + *subsystem = NULL; + *devname = NULL; /* Skip the header line; ACTION= and DEVPATH= repeat it as proper fields. */ pos = SDL_strlen(buf) + 1; @@ -225,39 +533,40 @@ static SDL_bool ParseUevent(char *buf, size_t len, SDL_webOSUevent *event) size_t field_len = SDL_strlen(field); if (SDL_strncmp(field, "ACTION=", 7) == 0) { - const char *action = field + 7; - if (SDL_strcmp(action, "add") == 0) { - event->action = SDL_WEBOS_UEVENT_ACTION_ADD; - } else if (SDL_strcmp(action, "remove") == 0) { - event->action = SDL_WEBOS_UEVENT_ACTION_REMOVE; + const char *value = field + 7; + if (SDL_strcmp(value, "add") == 0) { + *action = SDL_WEBOS_UEVENT_ACTION_ADD; + } else if (SDL_strcmp(value, "remove") == 0) { + *action = SDL_WEBOS_UEVENT_ACTION_REMOVE; } else { return SDL_FALSE; } + have_action = SDL_TRUE; } else if (SDL_strncmp(field, "SUBSYSTEM=", 10) == 0) { - event->subsystem = field + 10; + *subsystem = field + 10; } else if (SDL_strncmp(field, "DEVPATH=", 8) == 0) { - event->devpath = field + 8; + devpath = field + 8; } else if (SDL_strncmp(field, "DEVNAME=", 8) == 0) { /* Present whenever the event describes an actual device node, * and more trustworthy than the sysfs path, which for some * subsystems ends in the parent rather than the node. */ - devname = field + 8; + node = field + 8; } pos += field_len + 1; } - if (event->action == SDL_WEBOS_UEVENT_ACTION_OTHER) { + if (!have_action) { return SDL_FALSE; } /* DEVNAME arrives relative to /dev and may be nested ("input/event14"), * so reduce either source to the trailing component. A remove event on * an older kernel can omit DEVNAME, hence the DEVPATH fallback. */ - if (devname != NULL) { - event->devname = TrailingName(devname); - } else if (event->devpath != NULL) { - event->devname = TrailingName(event->devpath); + if (node != NULL) { + *devname = TrailingName(node); + } else if (devpath != NULL) { + *devname = TrailingName(devpath); } return SDL_TRUE; diff --git a/src/joystick/webos/uevent_monitor.h b/src/joystick/webos/uevent_monitor.h index 12a06e0a9c567..99f74142ddb48 100644 --- a/src/joystick/webos/uevent_monitor.h +++ b/src/joystick/webos/uevent_monitor.h @@ -3,6 +3,8 @@ #ifndef SDL_webos_uevent_monitor_h_ #define SDL_webos_uevent_monitor_h_ +#include "dev_presence.h" + /* Hotplug notifications straight from the kernel, via a * NETLINK_KOBJECT_UEVENT socket. * @@ -10,12 +12,29 @@ * backends otherwise fall back to rescanning /dev every 3 seconds and * diffing a presence bitmask. That's slow to react, and it can't see a * device that disconnects and reconnects on the same index between two - * scans, because the bitmask comes out unchanged. + * scans, because the bitmask comes out unchanged. Measured on hardware, a + * whole connect/disconnect cycle can land between two scans and leave no + * trace at all. * * The kernel broadcasts add/remove as uevents to anyone bound to group 1, - * with no privilege and no libudev needed. Events are explicit and ordered, - * so the same-index case stops being invisible, and they arrive in well - * under 100ms. + * with no privilege and no libudev needed. Verified working inside the app + * jail on webOS 3.4 (kernel 3.10), 4 (4.4) and 10 (5.4). + * + * Usage is meant to be no harder than the poll it replaces: open a monitor + * for the node class you care about, then drain it whenever you would have + * polled. Everything else -- enumerating what was already attached, + * filtering to the nodes you asked for, and recovering if the kernel drops + * events -- happens inside. + * + * monitor = SDL_webOSUeventMonitorOpen(SDL_WEBOS_DEVICE_PRESENCE_CHECK_EVDEV); + * + * while (SDL_webOSUeventMonitorPoll(monitor, &event)) { + * if (event.action == SDL_WEBOS_UEVENT_ACTION_ADD) { + * MaybeAddDevice(event.devnode); + * } else { + * MaybeRemoveDevice(event.devnode); + * } + * } * * Each subsystem should open its own monitor. Netlink broadcasts a copy to * every bound socket, so two monitors don't compete; sharing one would mean @@ -24,7 +43,6 @@ typedef enum SDL_webOSUeventAction { - SDL_WEBOS_UEVENT_ACTION_OTHER, SDL_WEBOS_UEVENT_ACTION_ADD, SDL_WEBOS_UEVENT_ACTION_REMOVE, } SDL_webOSUeventAction; @@ -35,50 +53,38 @@ typedef struct SDL_webOSUevent { SDL_webOSUeventAction action; - /* "input", "hidraw", ... NULL if the event didn't carry a SUBSYSTEM. */ - const char *subsystem; - - /* Trailing name of the device node, e.g. "event14", "js7", "hidraw0". - * NULL for events that don't describe a node (bus/class-level events). - * Prefix with the right directory to get a path; don't infer ordering - * from it, and key on the name itself. */ + /* Node name, e.g. "event14", "js7", "hidraw0". */ const char *devname; - /* Full DEVPATH under /sys, for logging. NULL if absent. */ - const char *devpath; + /* Full path, e.g. "/dev/input/event14". Ready to hand to + * MaybeAddDevice()/MaybeRemoveDevice() without any string building. */ + const char *devnode; } SDL_webOSUevent; -/* Returns NULL if the socket can't be created or bound, which the caller - * must treat as a normal outcome and handle by keeping the presence-flag - * poll. Unprivileged bind is the long-standing default and is verified - * working on webOS 10 (kernel 5.4), but it has not been confirmed on the - * 3.10 kernels that older webOS versions ship. */ -extern SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(void); +/* Opens a monitor for one class of device node. Binds the socket first and + * only then enumerates what's already attached, so a device appearing in + * between isn't missed; those existing devices come back out of Poll() as + * ordinary add events, so callers need no separate startup scan. + * + * Returns NULL if the socket can't be created or bound, which callers must + * treat as a normal outcome and handle by keeping the presence-flag poll. */ +extern SDL_webOSUeventMonitor *SDL_webOSUeventMonitorOpen(SDL_webOSDevicePresenceCheck watch); extern void SDL_webOSUeventMonitorClose(SDL_webOSUeventMonitor *monitor); -/* Reads one pending event, without blocking. Returns SDL_FALSE once the - * socket is drained; call it in a loop, since the socket buffer is finite - * and a burst can queue several events. +/* Reads one pending event, without blocking. Returns SDL_FALSE once there's + * nothing left; call it in a loop, since a burst can queue several. * - * The strings in `event` point into storage owned by the monitor and stay - * valid only until the next call on the same monitor. */ -extern SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUevent *event); - -/* Reports whether the kernel dropped uevents because the socket buffer filled - * up, and clears the condition. + * Only reports the node class the monitor was opened for. Strings in `event` + * stay valid until the next call on the same monitor. * - * Netlink is lossy under pressure: rather than blocking the sender, the kernel - * discards broadcasts and reports ENOBUFS once. The events are gone for good, - * so the device list can be wrong in a way no later event will correct -- a - * device removed during the gap simply never gets its remove. - * - * That matters here because a webOS app can be backgrounded or suspended, and - * a process that isn't draining the socket while controllers come and go is - * exactly how the buffer fills. Callers must treat a true return as "resync - * now" and redo the full scan they did at init. Ignoring it would leave the - * list permanently stale, which is worse than the polling it replaces, since - * polling recovers on its own within one interval. */ -extern SDL_bool SDL_webOSUeventMonitorLostEvents(SDL_webOSUeventMonitor *monitor); + * Netlink is lossy under pressure: rather than blocking the sender, the + * kernel discards broadcasts and reports ENOBUFS once. That's handled here + * rather than exposed -- the monitor re-enumerates and reports the + * difference against what it last told the caller, so the caller still sees + * a correct add/remove stream and never has to know it happened. It matters + * because a webOS app can be backgrounded or suspended, and a process that + * isn't draining while devices come and go is exactly how the buffer fills. */ +extern SDL_bool SDL_webOSUeventMonitorPoll(SDL_webOSUeventMonitor *monitor, SDL_webOSUevent *event); #endif /* SDL_webos_uevent_monitor_h_ */ diff --git a/test/testwebosuevent.c b/test/testwebosuevent.c index c93f5d1d499cf..f7496414410a9 100644 --- a/test/testwebosuevent.c +++ b/test/testwebosuevent.c @@ -16,23 +16,20 @@ * same hotplug activity, and reports whether netlink can replace the poll on * this device. No video, no window: it's meant to be run over SSH on a TV. * - * The question it exists to answer is whether an unprivileged process can - * receive kernel uevents on a given webOS version. That's confirmed on - * webOS 10 (kernel 5.4) but not on the 3.10 kernels older versions ship, and - * it's the one thing standing between the netlink monitor and dropping the - * 3-second poll. - * - * Note that it can only answer the question if devices actually come and go - * while it runs, which is why it reports INCONCLUSIVE rather than a pass when - * nothing happened. Silence is not evidence that netlink works: an idle - * system produces no uevents whether or not the socket is delivering them. + * It can only answer that if devices actually come and go while it runs, + * which is why it reports INCONCLUSIVE rather than a pass when nothing + * happened. Silence is not evidence that netlink works: an idle system + * produces no uevents whether or not the socket is delivering them. * * Usage: - * testwebosuevent [--duration SECONDS] [--poll-interval MS] [--verbose] + * testwebosuevent [--duration SECONDS] [--poll-interval MS] [--sdl] + * + * --sdl reports SDL's own joystick device events instead, which checks the + * backend wiring rather than the monitor underneath it. * * Plug and unplug a controller (USB or Bluetooth) a few times while it runs. - * Reconnecting the same controller repeatedly is the interesting case, since - * that's what the presence bitmask can't see. + * Cycling one faster than the poll interval is the interesting case, since + * that's what the presence bitmask can't represent. * * Exit status: 0 netlink usable, 1 fallback required, 2 inconclusive. */ @@ -65,7 +62,6 @@ typedef enum { - NODE_OTHER = -1, NODE_EVDEV = 0, NODE_JS, NODE_HIDRAW, @@ -78,11 +74,14 @@ typedef struct const char *prefix; SDL_webOSDevicePresenceCheck check; + /* One monitor per class, which is how a backend uses this: netlink + * delivers a copy to every bound socket, so they don't compete. */ + SDL_webOSUeventMonitor *monitor; + Uint32 poll_flags; - /* Indices netlink reported on since the last poll tick, and when it first - * reported each, so we can tell what the poll would have missed and how - * far ahead netlink was. */ + /* When netlink first reported each index since the last poll tick, so we + * can say how far ahead of the poll it was. */ Uint32 netlink_touched; Uint32 netlink_time[PRESENCE_MAX_INDEX]; @@ -94,22 +93,20 @@ typedef struct } NodeClass; static NodeClass node_classes[NODE_KIND_COUNT] = { - { "evdev", "event", SDL_WEBOS_DEVICE_PRESENCE_CHECK_EVDEV, 0, 0, { 0 } }, - { "js", "js", SDL_WEBOS_DEVICE_PRESENCE_CHECK_JS, 0, 0, { 0 } }, - { "hidraw", "hidraw", SDL_WEBOS_DEVICE_PRESENCE_CHECK_HIDRAW, 0, 0, { 0 } }, + { "evdev", "event", SDL_WEBOS_DEVICE_PRESENCE_CHECK_EVDEV, NULL, 0, 0, { 0 }, { 0 } }, + { "js", "js", SDL_WEBOS_DEVICE_PRESENCE_CHECK_JS, NULL, 0, 0, { 0 }, { 0 } }, + { "hidraw", "hidraw", SDL_WEBOS_DEVICE_PRESENCE_CHECK_HIDRAW, NULL, 0, 0, { 0 }, { 0 } }, }; /* Verdict inputs */ -static int poll_changes = 0; /* bitmask transitions the poll saw */ -static int poll_changes_missed = 0; /* ... that netlink never reported */ -static int netlink_events = 0; /* add/remove on a device node */ -static int netlink_invisible = 0; /* ... the bitmask diff could not express */ -static int beyond_bitmask = 0; /* nodes the 32-bit mask can't represent */ -static int netlink_overflows = 0; /* times the kernel dropped queued uevents */ -static Uint32 latency_total = 0; /* how far netlink led the poll, summed */ +static int poll_changes = 0; /* bitmask transitions the poll saw */ +static int poll_changes_missed = 0; /* ... that netlink never reported */ +static int netlink_events = 0; /* add/remove on a device node */ +static int netlink_invisible = 0; /* ... the bitmask diff could not express */ +static int beyond_bitmask = 0; /* nodes the 32-bit mask can't represent */ +static Uint32 latency_total = 0; /* how far netlink led the poll, summed */ static int latency_samples = 0; -static int verbose = 0; static volatile int keep_running = 1; static Uint32 start_time; @@ -142,72 +139,34 @@ static void Report(const char *fmt, ...) fflush(stdout); } -/* Splits "event14" into its class and index. Returns NODE_OTHER for anything - * that isn't a numbered node we track, including "mice" and "mouse0". */ -static NodeKind ClassifyNode(const char *devname, int *index) +/* The monitor only reports its own node class, so all that's left is pulling + * the index off the end of the name. */ +static int NodeIndex(const NodeClass *cls, const char *devname) { - int kind; - - if (devname == NULL) { - return NODE_OTHER; - } - - for (kind = 0; kind < NODE_KIND_COUNT; kind++) { - const char *prefix = node_classes[kind].prefix; - size_t prefix_len = SDL_strlen(prefix); - const char *suffix; - char *endptr = NULL; - long value; - - if (SDL_strncmp(devname, prefix, prefix_len) != 0) { - continue; - } - - suffix = devname + prefix_len; - - if (*suffix == '\0') { - continue; - } - - value = SDL_strtol(suffix, &endptr, 10); - - if (endptr == NULL || *endptr != '\0' || value < 0) { - continue; - } + const char *suffix = devname + SDL_strlen(cls->prefix); + char *endptr = NULL; + long value = SDL_strtol(suffix, &endptr, 10); - /* "js" is a prefix of nothing else here, but "event" vs "mouse" and - * friends means we only get here on an exact prefix + digits match. */ - *index = (int)value; - return (NodeKind)kind; + if (endptr == NULL || *endptr != '\0' || value < 0) { + return -1; } - return NODE_OTHER; + return (int)value; } -static void HandleUevent(const SDL_webOSUevent *event) +static void HandleUevent(NodeKind kind, const SDL_webOSUevent *event) { - NodeKind kind; - NodeClass *cls; - int index = 0; - - kind = ClassifyNode(event->devname, &index); - - if (kind == NODE_OTHER) { - if (verbose) { - Report("netlink %-6s %-8s %s (ignored)", - event->action == SDL_WEBOS_UEVENT_ACTION_ADD ? "add" : "remove", - event->subsystem ? event->subsystem : "-", - event->devname ? event->devname : "-"); - } + NodeClass *cls = &node_classes[kind]; + int index = NodeIndex(cls, event->devname); + + if (index < 0) { return; } - cls = &node_classes[kind]; netlink_events++; - Report("netlink %-6s %s/%d", - event->action == SDL_WEBOS_UEVENT_ACTION_ADD ? "add" : "remove", - cls->label, index); + Report("netlink %-6s %s", event->action == SDL_WEBOS_UEVENT_ACTION_ADD ? "add" : "remove", + event->devnode); if (index >= PRESENCE_MAX_INDEX) { /* The poll cannot see this device at all, on any timescale. */ @@ -227,6 +186,24 @@ static void HandleUevent(const SDL_webOSUevent *event) } } +/* Drains every monitor. `announce` is false while priming, where the monitors + * report everything already attached and there's nothing to compare against + * yet. */ +static void DrainMonitors(SDL_bool announce) +{ + int kind; + + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + SDL_webOSUevent event; + + while (SDL_webOSUeventMonitorPoll(node_classes[kind].monitor, &event)) { + if (announce) { + HandleUevent((NodeKind)kind, &event); + } + } + } +} + /* `announce` is false for the initial seeding scan, where every attached * device shows up as a change against an empty bitmask. Counting or printing * those would report the entire existing device list as arrivals that netlink @@ -285,7 +262,7 @@ static void PollPresence(SDL_bool announce) } } -static int PrintVerdict(SDL_bool monitor_opened) +static int PrintVerdict(SDL_bool monitors_opened) { printf("\n"); printf("=========================================================\n"); @@ -293,9 +270,8 @@ static int PrintVerdict(SDL_bool monitor_opened) printf(" poll-observed changes : %d\n", poll_changes); printf(" corroborated by netlink : %d\n", poll_changes - poll_changes_missed); printf(" missed by netlink : %d\n", poll_changes_missed); - printf(" transitions the poll cannot see : %d\n", netlink_invisible); + printf(" transitions the poll cannot see : %d\n", netlink_invisible); printf(" nodes beyond the 32-bit mask : %d\n", beyond_bitmask); - printf(" socket overflows (events lost) : %d\n", netlink_overflows); if (latency_samples > 0) { printf(" mean netlink lead over poll : %ums over %d samples\n", @@ -304,7 +280,7 @@ static int PrintVerdict(SDL_bool monitor_opened) printf("---------------------------------------------------------\n"); - if (!monitor_opened) { + if (!monitors_opened) { printf(" VERDICT: FALLBACK REQUIRED\n"); printf(" The netlink socket could not be opened or bound, so this\n"); printf(" webOS version must keep the presence poll.\n"); @@ -341,12 +317,56 @@ static int PrintVerdict(SDL_bool monitor_opened) return 0; } +/* Exercises the joystick backend rather than the monitor: brings up + * SDL_INIT_JOYSTICK and reports the device events SDL itself produces. That's + * what an application sees, so it's the check that the backend is really + * wired to the uevent stream and not just that the stream works. */ +static int RunSdlMode(Uint32 duration_ms) +{ + if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0) { + fprintf(stderr, "SDL_InitSubSystem(JOYSTICK) failed: %s\n", SDL_GetError()); + return 3; + } + + Report("SDL joystick subsystem up, %d joystick(s) already present", SDL_NumJoysticks()); + Report("running for %us — plug and unplug a controller now", duration_ms / 1000); + + while (keep_running && Elapsed() < duration_ms) { + SDL_Event event; + + SDL_PumpEvents(); + + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_JOYDEVICEADDED: + Report("SDL_JOYDEVICEADDED device index %d (%s)", event.jdevice.which, + SDL_JoystickNameForIndex(event.jdevice.which)); + break; + case SDL_JOYDEVICEREMOVED: + Report("SDL_JOYDEVICEREMOVED instance id %d", event.jdevice.which); + break; + default: + break; + } + } + + SDL_Delay(TICK_INTERVAL_MS); + } + + Report("%d joystick(s) present at exit", SDL_NumJoysticks()); + SDL_QuitSubSystem(SDL_INIT_JOYSTICK); + + return 0; +} + int main(int argc, char *argv[]) { - SDL_webOSUeventMonitor *monitor; + SDL_bool monitors_opened = SDL_TRUE; + SDL_bool sdl_mode = SDL_FALSE; Uint32 duration_ms = 30000; Uint32 poll_interval_ms = 3000; Uint32 last_poll; + int kind; int i; for (i = 1; i < argc; i++) { @@ -354,10 +374,10 @@ int main(int argc, char *argv[]) duration_ms = (Uint32)SDL_atoi(argv[++i]) * 1000; } else if (SDL_strcmp(argv[i], "--poll-interval") == 0 && i + 1 < argc) { poll_interval_ms = (Uint32)SDL_atoi(argv[++i]); - } else if (SDL_strcmp(argv[i], "--verbose") == 0) { - verbose = 1; + } else if (SDL_strcmp(argv[i], "--sdl") == 0) { + sdl_mode = SDL_TRUE; } else { - fprintf(stderr, "Usage: %s [--duration SECONDS] [--poll-interval MS] [--verbose]\n", argv[0]); + fprintf(stderr, "Usage: %s [--duration SECONDS] [--poll-interval MS] [--sdl]\n", argv[0]); return 3; } } @@ -371,7 +391,7 @@ int main(int argc, char *argv[]) signal(SIGTERM, OnSignal); /* No video, no joystick backend: this exercises the mechanism directly, - * so it stays meaningful before the backends are wired up to it. */ + * the same way a backend would. */ if (SDL_Init(0) < 0) { fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); return 3; @@ -381,46 +401,41 @@ int main(int argc, char *argv[]) start_time = SDL_GetTicks(); - monitor = SDL_webOSUeventMonitorOpen(); + if (sdl_mode) { + int result = RunSdlMode(duration_ms); + SDL_Quit(); + return result; + } - Report("netlink monitor: %s", monitor ? "bound to the kernel uevent group" : "UNAVAILABLE"); + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + node_classes[kind].monitor = SDL_webOSUeventMonitorOpen(node_classes[kind].check); + + if (node_classes[kind].monitor == NULL) { + monitors_opened = SDL_FALSE; + } + } - /* Seed the poll state before announcing readiness, so devices that were - * already attached don't register as arrivals on the first tick. The real - * backend has to bind the socket before this scan for the same reason: - * anything appearing in the gap would otherwise go unnoticed. */ + Report("netlink monitors: %s", + monitors_opened ? "bound to the kernel uevent group" : "UNAVAILABLE"); + + /* The monitors report everything already attached as adds. Discard those + * and seed the poll to match, so the run starts from a common baseline + * and only real hotplug activity is compared. */ + DrainMonitors(SDL_FALSE); PollPresence(SDL_FALSE); last_poll = SDL_GetTicks(); - { - int kind; - for (kind = 0; kind < NODE_KIND_COUNT; kind++) { - Report("already attached: %s %s", node_classes[kind].label, - node_classes[kind].poll_flags ? "yes" : "none"); - } + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + Report("already attached: %s %s", node_classes[kind].label, + node_classes[kind].poll_flags ? "yes" : "none"); } Report("running for %us — plug and unplug a controller now (Ctrl-C to stop early)", duration_ms / 1000); while (keep_running && Elapsed() < duration_ms) { - if (monitor != NULL) { - SDL_webOSUevent event; - - while (SDL_webOSUeventMonitorPoll(monitor, &event)) { - HandleUevent(&event); - } - - /* An overflow means the device list can be stale in a way no - * later event corrects, so a backend has to resync here. Counted - * separately because it invalidates the comparison rather than - * being a netlink failure. */ - if (SDL_webOSUeventMonitorLostEvents(monitor)) { - netlink_overflows++; - Report("netlink *** dropped events (buffer overflow) — a backend must rescan here ***"); - } - } + DrainMonitors(SDL_TRUE); if (SDL_TICKS_PASSED(SDL_GetTicks(), last_poll + poll_interval_ms)) { PollPresence(SDL_TRUE); @@ -430,12 +445,17 @@ int main(int argc, char *argv[]) SDL_Delay(TICK_INTERVAL_MS); } - /* A final poll, so a change in the last interval still gets cross-checked - * instead of being dropped on the floor at exit. */ + /* A final drain and poll, so a change in the last interval still gets + * cross-checked instead of being dropped on the floor at exit. */ + DrainMonitors(SDL_TRUE); PollPresence(SDL_TRUE); - SDL_webOSUeventMonitorClose(monitor); + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + SDL_webOSUeventMonitorClose(node_classes[kind].monitor); + node_classes[kind].monitor = NULL; + } + SDL_Quit(); - return PrintVerdict(monitor != NULL); + return PrintVerdict(monitors_opened); } diff --git a/test/testwebosuevent_parse.c b/test/testwebosuevent_parse.c index ea906461edaf5..ffe047fca910c 100644 --- a/test/testwebosuevent_parse.c +++ b/test/testwebosuevent_parse.c @@ -31,6 +31,27 @@ static int failures; +/* ParseUevent now hands back the raw fields; the monitor turns those into a + * SDL_webOSUevent once it knows the node class it's watching. Reassemble one + * here so the cases below stay readable. */ +static SDL_bool Parse(char *buf, size_t len, SDL_webOSUevent *event) +{ + const char *subsystem = NULL; + const char *devname = NULL; + SDL_webOSUeventAction action; + + SDL_zerop(event); + + if (!ParseUevent(buf, len, &subsystem, &devname, &action)) { + return SDL_FALSE; + } + + event->action = action; + event->devname = devname; + + return SDL_TRUE; +} + /* Assembles the NUL-separated field list the kernel actually sends. */ static size_t BuildUevent(char *buf, const char *const *fields, int count) { @@ -80,9 +101,8 @@ int main(int argc, char *argv[]) }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); Check("evdev add -> event14", - ParseUevent(buf, len, &ev) && + Parse(buf, len, &ev) && ev.action == SDL_WEBOS_UEVENT_ACTION_ADD && - SDL_strcmp(ev.subsystem, "input") == 0 && SDL_strcmp(ev.devname, "event14") == 0, ev.devname); } @@ -98,7 +118,7 @@ int main(int argc, char *argv[]) }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); Check("remove without DEVNAME -> DEVPATH fallback", - ParseUevent(buf, len, &ev) && + Parse(buf, len, &ev) && ev.action == SDL_WEBOS_UEVENT_ACTION_REMOVE && SDL_strcmp(ev.devname, "js7") == 0, ev.devname); @@ -115,8 +135,7 @@ int main(int argc, char *argv[]) }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); Check("hidraw add -> hidraw0", - ParseUevent(buf, len, &ev) && - SDL_strcmp(ev.subsystem, "hidraw") == 0 && + Parse(buf, len, &ev) && SDL_strcmp(ev.devname, "hidraw0") == 0, ev.devname); } @@ -132,7 +151,7 @@ int main(int argc, char *argv[]) "DEVNAME=input/event3", }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); - Check("change action rejected", !ParseUevent(buf, len, &ev), "accepted"); + Check("change action rejected", !Parse(buf, len, &ev), "accepted"); } /* bind/unbind arrive for these same devices on modern kernels. */ @@ -143,14 +162,14 @@ int main(int argc, char *argv[]) "SUBSYSTEM=input", }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); - Check("bind action rejected", !ParseUevent(buf, len, &ev), "accepted"); + Check("bind action rejected", !Parse(buf, len, &ev), "accepted"); } /* A libudev-format message must not be read as a kernel one. */ { SDL_memcpy(buf, "libudev\0", 8); SDL_memcpy(buf + 8, "ACTION=add\0", 11); - Check("libudev magic rejected", !ParseUevent(buf, 19, &ev), "accepted"); + Check("libudev magic rejected", !Parse(buf, 19, &ev), "accepted"); } { @@ -160,7 +179,7 @@ int main(int argc, char *argv[]) "DEVNAME=input/event3", }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); - Check("missing ACTION rejected", !ParseUevent(buf, len, &ev), "accepted"); + Check("missing ACTION rejected", !Parse(buf, len, &ev), "accepted"); } /* Bus- and class-level events are valid but describe no node. There must @@ -174,7 +193,7 @@ int main(int argc, char *argv[]) }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); Check("trailing-slash DEVPATH -> NULL devname", - ParseUevent(buf, len, &ev) && ev.devname == NULL, + Parse(buf, len, &ev) && ev.devname == NULL, ev.devname); } @@ -190,7 +209,7 @@ int main(int argc, char *argv[]) }; len = BuildUevent(buf, fields, SDL_arraysize(fields)); Check("event40 parsed (beyond bitmask range)", - ParseUevent(buf, len, &ev) && SDL_strcmp(ev.devname, "event40") == 0, + Parse(buf, len, &ev) && SDL_strcmp(ev.devname, "event40") == 0, ev.devname); }