SDL: Use inotify for HIDAPI joystick enumeration if not using udev

From 2c3269152a9b363b0b67c035f79a745b2791b2dc Mon Sep 17 00:00:00 2001
From: Ludovico de Nittis <[EMAIL REDACTED]>
Date: Tue, 16 Feb 2021 12:39:48 +0100
Subject: [PATCH] Use inotify for HIDAPI joystick enumeration if not using udev

This improves SDL's ability to detect HIDAPI joystick hotplug in a
container environment because we cannot reliably receive events from
udev in a container.

For a more detailed explanation of why this issue happens with
containers, please check the previous commit
"joystick: Use inotify to detect joystick unplug if not using udev"
(b0eba1c5).

Signed-off-by: Ludovico de Nittis <ludovico.denittis@collabora.com>
---
 src/joystick/hidapi/SDL_hidapijoystick.c | 125 +++++++++++++++++++++++
 1 file changed, 125 insertions(+)

diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c
index e751bb15f..25eb7b6dc 100644
--- a/src/joystick/hidapi/SDL_hidapijoystick.c
+++ b/src/joystick/hidapi/SDL_hidapijoystick.c
@@ -52,6 +52,13 @@
 #ifdef SDL_USE_LIBUDEV
 #include <poll.h>
 #endif
+#ifdef HAVE_INOTIFY
+#include <errno.h>              /* errno, strerror */
+#include <fcntl.h>
+#include <limits.h>             /* For the definition of NAME_MAX */
+#include <sys/inotify.h>
+#include <unistd.h>
+#endif
 #endif
 
 typedef enum
@@ -101,6 +108,7 @@ static SDL_HIDAPI_Device *SDL_HIDAPI_devices;
 static int SDL_HIDAPI_numjoysticks = 0;
 static SDL_bool initialized = SDL_FALSE;
 static SDL_bool shutting_down = SDL_FALSE;
+static int inotify_fd = -1;
 
 #if defined(SDL_USE_LIBUDEV)
 static const SDL_UDEV_Symbols * usyms = NULL;
@@ -194,6 +202,46 @@ static void CallbackIOServiceFunc(void *context, io_iterator_t portIterator)
 }
 #endif /* __MACOSX__ */
 
+#ifdef HAVE_INOTIFY
+#ifdef HAVE_INOTIFY_INIT1
+static int SDL_inotify_init1(void) {
+    return inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
+}
+#else
+static int SDL_inotify_init1(void) {
+    int fd = inotify_init();
+    if (fd  < 0) return -1;
+    fcntl(fd, F_SETFL, O_NONBLOCK);
+    fcntl(fd, F_SETFD, FD_CLOEXEC);
+    return fd;
+}
+#endif
+
+static int
+StrHasPrefix(const char *string, const char *prefix)
+{
+    return (SDL_strncmp(string, prefix, SDL_strlen(prefix)) == 0);
+}
+
+static int
+StrIsInteger(const char *string)
+{
+    const char *p;
+
+    if (*string == '\0') {
+        return 0;
+    }
+
+    for (p = string; *p != '\0'; p++) {
+        if (*p < '0' || *p > '9') {
+            return 0;
+        }
+    }
+
+    return 1;
+}
+#endif
+
 static void
 HIDAPI_InitializeDiscovery()
 {
@@ -301,7 +349,37 @@ HIDAPI_InitializeDiscovery()
             }
         }
     }
+    else
 #endif /* SDL_USE_LIBUDEV */
+    {
+#if defined(HAVE_INOTIFY)
+        inotify_fd = SDL_inotify_init1();
+
+        if (inotify_fd < 0) {
+            SDL_LogWarn(SDL_LOG_CATEGORY_INPUT,
+                        "Unable to initialize inotify, falling back to polling: %s",
+                        strerror(errno));
+            return;
+        }
+
+        /* We need to watch for attribute changes in addition to
+         * creation, because when a device is first created, it has
+         * permissions that we can't read. When udev chmods it to
+         * something that we maybe *can* read, we'll get an
+         * IN_ATTRIB event to tell us. */
+        if (inotify_add_watch(inotify_fd, "/dev",
+                              IN_CREATE | IN_DELETE | IN_MOVE | IN_ATTRIB) < 0) {
+            close(inotify_fd);
+            inotify_fd = -1;
+            SDL_LogWarn(SDL_LOG_CATEGORY_INPUT,
+                        "Unable to add inotify watch, falling back to polling: %s",
+                        strerror (errno));
+            return;
+        }
+
+        SDL_HIDAPI_discovery.m_bCanGetNotifications = SDL_TRUE;
+#endif /* HAVE_INOTIFY */
+    }
 }
 
 static void
@@ -368,7 +446,49 @@ HIDAPI_UpdateDiscovery()
             }
         }
     }
+    else
 #endif /* SDL_USE_LIBUDEV */
+    {
+#if defined(HAVE_INOTIFY)
+        if (inotify_fd >= 0) {
+            union
+            {
+                struct inotify_event event;
+                char storage[4096];
+                char enough_for_inotify[sizeof (struct inotify_event) + NAME_MAX + 1];
+            } buf;
+            ssize_t bytes;
+            size_t remain = 0;
+            size_t len;
+
+            bytes = read(inotify_fd, &buf, sizeof (buf));
+
+            if (bytes > 0) {
+                remain = (size_t) bytes;
+            }
+
+            while (remain > 0) {
+                if (buf.event.len > 0 &&
+                    !SDL_HIDAPI_discovery.m_bHaveDevicesChanged) {
+                    if (StrHasPrefix(buf.event.name, "hidraw") &&
+                        StrIsInteger(buf.event.name + strlen ("hidraw"))) {
+                        SDL_HIDAPI_discovery.m_bHaveDevicesChanged = SDL_TRUE;
+                        /* We found an hidraw change. We still continue to
+                         * drain the inotify fd to avoid leaving old
+                         * notifications in the queue. */
+                    }
+                }
+
+                len = sizeof (struct inotify_event) + buf.event.len;
+                remain -= len;
+
+                if (remain != 0) {
+                    memmove(&buf.storage[0], &buf.storage[len], remain);
+                }
+            }
+        }
+#endif /* HAVE_INOTIFY */
+    }
 }
 
 static void
@@ -1283,6 +1403,11 @@ HIDAPI_JoystickQuit(void)
 
     SDL_HIDAPI_QuitRumble();
 
+    if (inotify_fd >= 0) {
+        close(inotify_fd);
+        inotify_fd = -1;
+    }
+
     while (SDL_HIDAPI_devices) {
         HIDAPI_DelDevice(SDL_HIDAPI_devices);
     }