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 new file mode 100644 index 0000000000000..4a10c47c9a08f --- /dev/null +++ b/src/joystick/webos/uevent_monitor.c @@ -0,0 +1,581 @@ +#include "uevent_monitor.h" + +#include +#include +#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 + +/* "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; + + 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, const char **subsystem, const char **devname, + SDL_webOSUeventAction *action); + +static const char *TrailingName(const char *path); + +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) { + 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; + 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; +} + +void SDL_webOSUeventMonitorClose(SDL_webOSUeventMonitor *monitor) +{ + if (monitor == NULL) { + return; + } + + if (monitor->fd >= 0) { + close(monitor->fd); + } + + SDL_free(monitor->known); + SDL_free(monitor->queue); + 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; + 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. */ + 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; + } + + if (bytes < 0 && errno == ENOBUFS) { + /* 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), re-enumerating %s/%s*", + monitor->base_dir, monitor->prefix); + Resynchronize(monitor); + 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, &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); + } + + EmitEvent(monitor, action, devname, event); + return SDL_TRUE; + } +} + +/* 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) +{ + char (*present)[NODE_NAME_SIZE] = NULL; + int present_count = 0; + int present_capacity = 0; + DIR *dir; + struct dirent *entry; + int i; + + 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; + } + + 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++; +} + +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) +{ + 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 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); + 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, const char **subsystem, const char **devname, + SDL_webOSUeventAction *action) +{ + 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 + * 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; + } + + *subsystem = NULL; + *devname = NULL; + + /* 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 *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) { + *subsystem = field + 10; + } else if (SDL_strncmp(field, "DEVPATH=", 8) == 0) { + 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. */ + node = field + 8; + } + + pos += field_len + 1; + } + + 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 (node != NULL) { + *devname = TrailingName(node); + } else if (devpath != NULL) { + *devname = TrailingName(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..99f74142ddb48 --- /dev/null +++ b/src/joystick/webos/uevent_monitor.h @@ -0,0 +1,90 @@ +#include "../../SDL_internal.h" + +#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. + * + * 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. 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. 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 + * whichever side drained it first consumed the other's events. + */ + +typedef enum SDL_webOSUeventAction +{ + 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; + + /* Node name, e.g. "event14", "js7", "hidraw0". */ + const char *devname; + + /* Full path, e.g. "/dev/input/event14". Ready to hand to + * MaybeAddDevice()/MaybeRemoveDevice() without any string building. */ + const char *devnode; +} SDL_webOSUevent; + +/* 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 there's + * nothing left; call it in a loop, since a burst can queue several. + * + * 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. 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/CMakeLists.txt b/test/CMakeLists.txt index 6388f9c0fbc19..70e2f07e1598b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -175,6 +175,27 @@ 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) + + 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) 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..f7496414410a9 --- /dev/null +++ b/test/testwebosuevent.c @@ -0,0 +1,461 @@ +/* + 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. + * + * 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] [--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. + * 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. + */ + +/* 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_EVDEV = 0, + NODE_JS, + NODE_HIDRAW, + NODE_KIND_COUNT +} NodeKind; + +typedef struct +{ + const char *label; + 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; + + /* 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]; + + /* 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] = { + { "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 Uint32 latency_total = 0; /* how far netlink led the poll, summed */ +static int latency_samples = 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); +} + +/* 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) +{ + const char *suffix = devname + SDL_strlen(cls->prefix); + char *endptr = NULL; + long value = SDL_strtol(suffix, &endptr, 10); + + if (endptr == NULL || *endptr != '\0' || value < 0) { + return -1; + } + + return (int)value; +} + +static void HandleUevent(NodeKind kind, const SDL_webOSUevent *event) +{ + NodeClass *cls = &node_classes[kind]; + int index = NodeIndex(cls, event->devname); + + if (index < 0) { + return; + } + + netlink_events++; + + 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. */ + 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; + + if (cls->netlink_transitions[index] < 255) { + cls->netlink_transitions[index]++; + } +} + +/* 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 + * failed to report. */ +static void PollPresence(SDL_bool announce) +{ + 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; + int index; + + for (index = 0; announce && 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); + } + } + + /* 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)); + } +} + +static int PrintVerdict(SDL_bool monitors_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(" transitions the poll cannot see : %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 (!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"); + 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(" 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"); + 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_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++) { + 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], "--sdl") == 0) { + sdl_mode = SDL_TRUE; + } else { + fprintf(stderr, "Usage: %s [--duration SECONDS] [--poll-interval MS] [--sdl]\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, + * the same way a backend would. */ + 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(); + + if (sdl_mode) { + int result = RunSdlMode(duration_ms); + SDL_Quit(); + return result; + } + + 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; + } + } + + 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(); + + 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) { + DrainMonitors(SDL_TRUE); + + if (SDL_TICKS_PASSED(SDL_GetTicks(), last_poll + poll_interval_ms)) { + PollPresence(SDL_TRUE); + last_poll = SDL_GetTicks(); + } + + SDL_Delay(TICK_INTERVAL_MS); + } + + /* 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); + + for (kind = 0; kind < NODE_KIND_COUNT; kind++) { + SDL_webOSUeventMonitorClose(node_classes[kind].monitor); + node_classes[kind].monitor = NULL; + } + + SDL_Quit(); + + return PrintVerdict(monitors_opened); +} diff --git a/test/testwebosuevent_parse.c b/test/testwebosuevent_parse.c new file mode 100644 index 0000000000000..ffe047fca910c --- /dev/null +++ b/test/testwebosuevent_parse.c @@ -0,0 +1,219 @@ +/* + 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; + +/* 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) +{ + 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", + Parse(buf, len, &ev) && + ev.action == SDL_WEBOS_UEVENT_ACTION_ADD && + 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", + Parse(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", + Parse(buf, len, &ev) && + 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", !Parse(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", !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", !Parse(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", !Parse(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", + Parse(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)", + Parse(buf, len, &ev) && SDL_strcmp(ev.devname, "event40") == 0, + ev.devname); + } + + printf("\n%s\n", failures == 0 ? "ALL PASS" : "FAILURES PRESENT"); + + return failures != 0; +}