diff --git a/CMakeLists.txt b/CMakeLists.txt index 37991e76..d050412b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ include(CheckIncludeFiles) find_package(Kodi REQUIRED) find_package(TinyXML2 REQUIRED) +find_package(Threads REQUIRED) include_directories(${PROJECT_SOURCE_DIR}/src ${KODI_INCLUDE_DIR}/..) # Hack way with "/..", need bigger Kodi cmake rework to match right include ways (becomes done in future) @@ -119,6 +120,7 @@ if(HAVE_SYSLOG) endif() list(APPEND DEPLIBS tinyxml2::tinyxml2) +list(APPEND DEPLIBS Threads::Threads) # --- SDL2 --------------------------------------------------------------------- diff --git a/src/api/linux/JoystickInterfaceLinux.cpp b/src/api/linux/JoystickInterfaceLinux.cpp index cb736705..1f9207c3 100644 --- a/src/api/linux/JoystickInterfaceLinux.cpp +++ b/src/api/linux/JoystickInterfaceLinux.cpp @@ -8,15 +8,21 @@ #include "JoystickInterfaceLinux.h" #include "JoystickLinux.h" +#include "api/JoystickManager.h" #include "api/JoystickTypes.h" #include "log/Log.h" +#include +#include #include #include #include #include #include +#include #include +#include +#include #include #include #include @@ -24,11 +30,136 @@ using namespace JOYSTICK; +namespace +{ + constexpr const char *DEV_INPUT_DIR = "/dev/input"; +} + EJoystickInterface CJoystickInterfaceLinux::Type(void) const { return EJoystickInterface::LINUX; } +bool CJoystickInterfaceLinux::Initialize(void) +{ + // Hotplug support discovers devices by watching /dev/input/js* files. + // Watch /dev/input with inotify so that joystick nodes being added or + // removed trigger an immediate rescan instead of waiting for the next + // periodic scan. + m_inotifyFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (m_inotifyFd < 0) + { + esyslog("%s: failed to initialize inotify (errno=%d)", __FUNCTION__, errno); + return true; + } + + // Listen for created, deleted, and attribute modifications. There is an issue discovered where a device + // won't have the correct permissions for opening on creation, so there will be two events, one on creation and + // one on permission set. + if (inotify_add_watch(m_inotifyFd, DEV_INPUT_DIR, IN_CREATE | IN_DELETE | IN_ATTRIB) < 0) + { + esyslog("%s: failed to watch %s (errno=%d)", __FUNCTION__, DEV_INPUT_DIR, errno); + close(m_inotifyFd); + m_inotifyFd = -1; + return true; + } + + // eventfd used to wake the monitor thread immediately on shutdown rather than relying on a poll timeout + // A failure returns -1, which polling will ignore. + m_stopFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (m_stopFd < 0) + esyslog("%s: failed to create eventfd (errno=%d)", __FUNCTION__, errno); + + m_bStop = false; + m_monitorThread = std::thread(&CJoystickInterfaceLinux::MonitorThreadProcess, this); + + return true; +} + +void CJoystickInterfaceLinux::Deinitialize(void) +{ + // Set stop and trigger shutdown event. After monitor thread finishes cleanup all resources + m_bStop = true; + if (m_stopFd >= 0) + { + const uint64_t one = 1; + if (write(m_stopFd, &one, sizeof(one)) < 0) + esyslog("%s: failed to signal monitor stop (errno=%d)", __FUNCTION__, errno); + } + + if (m_monitorThread.joinable()) + m_monitorThread.join(); + + if (m_stopFd >= 0) + { + close(m_stopFd); + m_stopFd = -1; + } + + if (m_inotifyFd >= 0) + { + close(m_inotifyFd); + m_inotifyFd = -1; + } +} + +void CJoystickInterfaceLinux::MonitorThreadProcess() +{ + alignas(struct inotify_event) char buf[4096]; + + while (!m_bStop) + { + struct pollfd pfds[2] = {}; + pfds[0].fd = m_inotifyFd; + pfds[0].events = POLLIN; + pfds[1].fd = m_stopFd; + pfds[1].events = POLLIN; + + // Block until inotify has data or shutdown is signalled via the eventfd + // If eventfd couldn't be created, use a finite timeout so shutdown can't hang forever. + const int timeoutMs = (m_stopFd >= 0) ? -1 : 1000; + const int rc = poll(pfds, 2, timeoutMs); + if (rc < 0) + { + if (errno == EINTR) + continue; + esyslog("%s: poll() failed (errno=%d)", __FUNCTION__, errno); + break; + } + + // Woken by the stop eventfd, break the loop immediately + if (pfds[1].revents & POLLIN) + break; + + if (!(pfds[0].revents & POLLIN)) + continue; + + bool bChanged = false; + + // Drain all queued events + ssize_t len; + while ((len = read(m_inotifyFd, buf, sizeof(buf))) > 0) + { + for (char *ptr = buf; ptr < buf + len;) + { + const struct inotify_event *event = reinterpret_cast(ptr); + + // We only care about joystick device changes. (/dev/input/js* files) + if (event->len > 0 && std::strncmp(event->name, "js", 2) == 0) + bChanged = true; + + ptr += sizeof(struct inotify_event) + event->len; + } + } + + if (bChanged) + { + CJoystickManager::Get().SetChanged(true); + CJoystickManager::Get().TriggerScan(); + } + } +} + bool CJoystickInterfaceLinux::ScanForJoysticks(JoystickVector& joysticks) { // TODO: Use udev to grab device names instead of reading /dev/input/js* diff --git a/src/api/linux/JoystickInterfaceLinux.h b/src/api/linux/JoystickInterfaceLinux.h index 24a931cc..505c054c 100644 --- a/src/api/linux/JoystickInterfaceLinux.h +++ b/src/api/linux/JoystickInterfaceLinux.h @@ -10,19 +10,34 @@ #include "api/IJoystickInterface.h" +#include #include #include +#include namespace JOYSTICK { class CJoystickInterfaceLinux : public IJoystickInterface { public: - CJoystickInterfaceLinux(void) { } - virtual ~CJoystickInterfaceLinux(void) { } + CJoystickInterfaceLinux(void) = default; + virtual ~CJoystickInterfaceLinux(void) { Deinitialize(); } // implementation of IJoystickInterface virtual EJoystickInterface Type(void) const override; + virtual bool Initialize(void) override; + virtual void Deinitialize(void) override; virtual bool ScanForJoysticks(JoystickVector& joysticks) override; + + private: + /*! + * \brief Hotplug thread that watches /dev/input with inotify + */ + void MonitorThreadProcess(); + + int m_inotifyFd{-1}; + int m_stopFd{-1}; //!< eventfd used to wake the monitor thread on shutdown + std::thread m_monitorThread; + std::atomic m_bStop{false}; }; } diff --git a/src/api/udev/JoystickInterfaceUdev.cpp b/src/api/udev/JoystickInterfaceUdev.cpp index 247bd15c..c62ca878 100644 --- a/src/api/udev/JoystickInterfaceUdev.cpp +++ b/src/api/udev/JoystickInterfaceUdev.cpp @@ -8,10 +8,18 @@ #include "JoystickInterfaceUdev.h" #include "JoystickUdev.h" +#include "api/JoystickManager.h" #include "api/JoystickTypes.h" #include "log/Log.h" +#include +#include +#include +#include #include +#include +#include +#include #include using namespace JOYSTICK; @@ -58,11 +66,40 @@ bool CJoystickInterfaceUdev::Initialize() udev_monitor_filter_add_match_subsystem_devtype(m_udev_mon, "input", nullptr); udev_monitor_enable_receiving(m_udev_mon); + // eventfd used to wake the monitor thread immediately on shutdown rather than relying on a poll timeout + // A failure returns -1, which polling will ignore. + m_stopFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (m_stopFd < 0) + esyslog("Failed to create eventfd for udev monitor (errno=%d)", errno); + + // Watch the monitor for hotplug events so devices are detected immediately + // instead of waiting for the next periodic scan + m_bStop = false; + m_monitorThread = std::thread(&CJoystickInterfaceUdev::MonitorThreadProcess, this); + return true; } void CJoystickInterfaceUdev::Deinitialize() { + // Set stop and trigger shutdown event. After monitor thread finishes cleanup all resources + m_bStop = true; + if (m_stopFd >= 0) + { + const uint64_t one = 1; + if (write(m_stopFd, &one, sizeof(one)) < 0) + esyslog("Failed to signal udev monitor stop (errno=%d)", errno); + } + + if (m_monitorThread.joinable()) + m_monitorThread.join(); + + if (m_stopFd >= 0) + { + close(m_stopFd); + m_stopFd = -1; + } + if (m_udev_mon) { udev_monitor_unref(m_udev_mon); @@ -76,6 +113,70 @@ void CJoystickInterfaceUdev::Deinitialize() } } +void CJoystickInterfaceUdev::MonitorThreadProcess() +{ + const int fd = udev_monitor_get_fd(m_udev_mon); + if (fd < 0) + { + esyslog("Failed to get udev monitor file descriptor"); + return; + } + + while (!m_bStop) + { + struct pollfd pfds[2] = {}; + pfds[0].fd = fd; + pfds[0].events = POLLIN; + pfds[1].fd = m_stopFd; + pfds[1].events = POLLIN; + + // Block until the monitor has data or shutdown is signalled via the eventfd. + // If eventfd couldn't be created, use a finite timeout so shutdown can't hang forever. + const int timeoutMs = (m_stopFd >= 0) ? -1 : 1000; + const int rc = poll(pfds, 2, timeoutMs); + if (rc < 0) + { + if (errno == EINTR) + continue; + esyslog("poll() failed on udev monitor (errno=%d)", errno); + break; + } + + // Woken by the stop eventfd, break the loop immediately + if (pfds[1].revents & POLLIN) + break; + + if (!(pfds[0].revents & POLLIN)) + continue; + + bool bChanged = false; + + // Drain all queued events + struct udev_device *dev; + while ((dev = udev_monitor_receive_device(m_udev_mon)) != nullptr) + { + const char *action = udev_device_get_action(dev); + const char *isJoystick = udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK"); + + // We only care about joysticks devices changes. + if (isJoystick != nullptr && std::strcmp(isJoystick, "1") == 0 && + action != nullptr && + (std::strcmp(action, "add") == 0 || std::strcmp(action, "remove") == 0)) + { + bChanged = true; + } + + udev_device_unref(dev); + } + + if (bChanged) + { + CJoystickManager::Get().SetChanged(true); + CJoystickManager::Get().TriggerScan(); + } + } +} + bool CJoystickInterfaceUdev::ScanForJoysticks(JoystickVector& joysticks) { if (!m_udev) diff --git a/src/api/udev/JoystickInterfaceUdev.h b/src/api/udev/JoystickInterfaceUdev.h index 6729f3cb..d239c3d8 100644 --- a/src/api/udev/JoystickInterfaceUdev.h +++ b/src/api/udev/JoystickInterfaceUdev.h @@ -30,6 +30,9 @@ #include "api/IJoystickInterface.h" +#include +#include + struct udev; struct udev_device; struct udev_monitor; @@ -51,9 +54,18 @@ namespace JOYSTICK virtual const ButtonMap& GetButtonMap() override; private: + /*! + * \brief Hotplug thread that watches the udev monitor + */ + void MonitorThreadProcess(); + udev* m_udev; udev_monitor* m_udev_mon; + int m_stopFd{-1}; //!< eventfd used to wake the monitor thread on shutdown + std::thread m_monitorThread; + std::atomic m_bStop{false}; + static ButtonMap m_buttonMap; }; }