Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -119,6 +120,7 @@ if(HAVE_SYSLOG)
endif()

list(APPEND DEPLIBS tinyxml2::tinyxml2)
list(APPEND DEPLIBS Threads::Threads)

# --- SDL2 ---------------------------------------------------------------------

Expand Down
131 changes: 131 additions & 0 deletions src/api/linux/JoystickInterfaceLinux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,158 @@

#include "JoystickInterfaceLinux.h"
#include "JoystickLinux.h"
#include "api/JoystickManager.h"
#include "api/JoystickTypes.h"
#include "log/Log.h"

#include <cstdint>
#include <cstring>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/input.h>
#include <linux/joystick.h>
#include <poll.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

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();
Comment thread
bjsvedin marked this conversation as resolved.

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<const struct inotify_event *>(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*
Expand Down
19 changes: 17 additions & 2 deletions src/api/linux/JoystickInterfaceLinux.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,34 @@

#include "api/IJoystickInterface.h"

#include <atomic>
#include <stdint.h>
#include <string>
#include <thread>

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<bool> m_bStop{false};
};
}
101 changes: 101 additions & 0 deletions src/api/udev/JoystickInterfaceUdev.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@

#include "JoystickInterfaceUdev.h"
#include "JoystickUdev.h"
#include "api/JoystickManager.h"
#include "api/JoystickTypes.h"
#include "log/Log.h"

#include <cstdint>
#include <cstring>
#include <errno.h>
#include <fcntl.h>
#include <libudev.h>
#include <poll.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <utility>

using namespace JOYSTICK;
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/api/udev/JoystickInterfaceUdev.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@

#include "api/IJoystickInterface.h"

#include <atomic>
#include <thread>

struct udev;
struct udev_device;
struct udev_monitor;
Expand All @@ -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<bool> m_bStop{false};

static ButtonMap m_buttonMap;
};
}