Replaced inotify joystick polling with udev monitoring, added more precise checking whenever connection/disconnection occurs so full scans are no longer needed, fixed up USB attribute querying and added udev property querying as the primary method of getting joystick property data, added a fallback method of getting the joystick name if JSIOCGNAME fails.

This commit is contained in:
binary1248 2015-03-23 14:12:23 +01:00 committed by Lukas Dürrenberger
parent 5ce73e9274
commit bbfef72fc6
2 changed files with 349 additions and 123 deletions

View File

@ -27,25 +27,136 @@
////////////////////////////////////////////////////////////
#include <SFML/Window/JoystickImpl.hpp>
#include <SFML/System/Err.hpp>
#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <errno.h>
#include <linux/joystick.h>
#include <libudev.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <errno.h>
#include <sstream>
#include <cstring>
namespace
{
int notifyFd = -1;
int inputFd = -1;
struct udev* udevContext = 0;
struct udev_monitor* udevMonitor = 0;
bool plugged[sf::Joystick::Count];
void updatePluggedList()
struct udev_device* getUdevDevice(const std::string& node)
{
udev* udevContext = udev_new();
struct stat statBuffer;
if (stat(node.c_str(), &statBuffer) < 0)
return NULL;
char deviceType;
if (S_ISBLK(statBuffer.st_mode))
deviceType = 'b';
else if (S_ISCHR(statBuffer.st_mode))
deviceType = 'c';
else
return NULL;
return udev_device_new_from_devnum(udevContext, deviceType, statBuffer.st_rdev);
}
int getJoystickNumberFromNode(std::string node)
{
std::string::size_type n = node.find("/js");
if (n != std::string::npos)
node = node.substr(n + 3);
else
return -1;
std::stringstream sstr(node);
int i = -1;
if (!(sstr >> i))
i = -1;
return i;
}
bool isJoystick(udev_device* udevDevice)
{
// If anything goes wrong, we go safe and return true
// No device to check, assume joystick
if (!udevDevice)
return true;
// Check if this device is a joystick
if (udev_device_get_property_value(udevDevice, "ID_INPUT_JOYSTICK"))
return true;
// Check if this device is something that isn't a joystick
// We do this because the absence of any ID_INPUT_ property doesn't
// necessarily mean that the device isn't a joystick, whereas the
// presence of any ID_INPUT_ property that isn't ID_INPUT_JOYSTICK does
if (udev_device_get_property_value(udevDevice, "ID_INPUT_ACCELEROMETER") ||
udev_device_get_property_value(udevDevice, "ID_INPUT_KEY") ||
udev_device_get_property_value(udevDevice, "ID_INPUT_KEYBOARD") ||
udev_device_get_property_value(udevDevice, "ID_INPUT_MOUSE") ||
udev_device_get_property_value(udevDevice, "ID_INPUT_TABLET") ||
udev_device_get_property_value(udevDevice, "ID_INPUT_TOUCHPAD") ||
udev_device_get_property_value(udevDevice, "ID_INPUT_TOUCHSCREEN"))
return false;
// On some platforms (older udev), ID_INPUT_ properties are not present, instead
// the system makes use of the ID_CLASS property to identify the device class
const char* idClass = udev_device_get_property_value(udevDevice, "ID_CLASS");
if (idClass)
{
// Check if the device class matches joystick
if (std::strstr(idClass, "joystick"))
return true;
// Check if the device class matches something that isn't a joystick
// Rationale same as above
if (std::strstr(idClass, "accelerometer") ||
std::strstr(idClass, "key") ||
std::strstr(idClass, "keyboard") ||
std::strstr(idClass, "mouse") ||
std::strstr(idClass, "tablet") ||
std::strstr(idClass, "touchpad") ||
std::strstr(idClass, "touchscreen"))
return false;
}
// At this point, assume it's a joystick
return true;
}
void updatePluggedList(udev_device* udevDevice = NULL)
{
if (udevDevice)
{
const char* node = udev_device_get_devnode(udevDevice);
if (node)
{
int i = getJoystickNumberFromNode(node);
if ((i >= 0) && (i < sf::Joystick::Count))
{
int file = ::open(node, O_RDONLY);
if (file < 0)
{
plugged[i] = false;
}
else
{
::close(file);
plugged[i] = isJoystick(udevDevice);
return;
}
}
}
}
for (unsigned int i = 0; i < sf::Joystick::Count; ++i)
{
@ -63,9 +174,8 @@ namespace
::close(file);
// Check if the device is really a joystick or an
// accelerometer by inspecting whether
// ID_INPUT_ACCELEROMETER is present
// Now we check if the device is really a joystick
if (!udevContext)
{
// Go safe and assume it is if udev isn't available
@ -82,104 +192,169 @@ namespace
continue;
}
if (udev_device_get_property_value(udevDevice, "ID_INPUT_ACCELEROMETER"))
{
// This device is an accelerometer
plugged[i] = false;
}
else
{
// This device is not an accelerometer
// Assume it's a joystick
plugged[i] = true;
}
plugged[i] = isJoystick(udevDevice);
udev_device_unref(udevDevice);
}
if (udevContext)
udev_unref(udevContext);
}
bool hasInotifyEvent()
bool hasMonitorEvent()
{
// This will not fail since we make sure udevMonitor is valid
int monitorFd = udev_monitor_get_fd(udevMonitor);
fd_set descriptorSet;
FD_ZERO(&descriptorSet);
FD_SET(notifyFd, &descriptorSet);
FD_SET(monitorFd, &descriptorSet);
timeval timeout = {0, 0};
return (select(notifyFd + 1, &descriptorSet, NULL, NULL, &timeout) > 0) &&
FD_ISSET(notifyFd, &descriptorSet);
return (select(monitorFd + 1, &descriptorSet, NULL, NULL, &timeout) > 0) &&
FD_ISSET(monitorFd, &descriptorSet);
}
// Get the joystick name
std::string getJoystickName(int file, unsigned int index)
// Get a property value from a udev device
const char* getUdevAttribute(udev_device* udevDevice, const std::string& attributeName)
{
// Get the name
char name[128];
if (ioctl(file, JSIOCGNAME(sizeof(name)), name) >= 0)
return std::string(name);
sf::err() << "Unable to get name for joystick at index " << index << std::endl;
return std::string("Unknown Joystick");
return udev_device_get_property_value(udevDevice, attributeName.c_str());
}
// Get a system attribute from a udev device as an unsigned int
unsigned int getUdevAttributeUint(udev_device* device, unsigned int index, const std::string& attributeName)
// Get a system attribute from a USB device
const char* getUsbAttribute(udev_device* udevDevice, const std::string& attributeName)
{
udev_device* udevDeviceParent = udev_device_get_parent_with_subsystem_devtype(device, "usb", "usb_device");
udev_device* udevDeviceParent = udev_device_get_parent_with_subsystem_devtype(udevDevice, "usb", "usb_device");
if (!udevDeviceParent)
{
sf::err() << "Unable to get joystick attribute. "
<< "Could not find parent USB device for joystick at index " << index << "." << std::endl;
return 0;
}
return NULL;
const char* attributeString = udev_device_get_sysattr_value(udevDeviceParent, attributeName.c_str());
if (!attributeString)
{
sf::err() << "Unable to get joystick attribute '" << attributeName << "'. "
<< "Attribute does not exist for joystick at index " << index << "." << std::endl;
return 0;
}
return static_cast<unsigned int>(std::strtoul(attributeString, NULL, 16));
return udev_device_get_sysattr_value(udevDeviceParent, attributeName.c_str());
}
// Get a system attribute for a joystick at index as an unsigned int
unsigned int getAttributeUint(unsigned int index, const std::string& attributeName)
// Get a system attribute for a joystick devnode as an unsigned int
unsigned int getUsbAttributeUint(const std::string& node, const std::string& attributeName)
{
udev* udevContext = udev_new();
if (!udevContext)
{
sf::err() << "Unable to get joystick attribute. "
<< "Could not create udev context." << std::endl;
return 0;
}
std::ostringstream sysname("js");
sysname << index;
udev_device* udevDevice = udev_device_new_from_subsystem_sysname(udevContext, "input", sysname.str().c_str());
udev_device* udevDevice = getUdevDevice(node);
if (!udevDevice)
{
sf::err() << "Unable to get joystick attribute. "
<< "Could not find USB device for joystick at index " << index << "." << std::endl;
udev_unref(udevContext);
<< "Could not get udev device for joystick " << node << std::endl;
return 0;
}
unsigned int attribute = getUdevAttributeUint(udevDevice, index, attributeName);
const char* attribute = getUsbAttribute(udevDevice, attributeName);
unsigned int value = 0;
if (attribute)
value = static_cast<unsigned int>(std::strtoul(attribute, NULL, 16));
udev_device_unref(udevDevice);
udev_unref(udevContext);
return attribute;
return value;
}
// Get a property value for a joystick devnode as an unsigned int
unsigned int getUdevAttributeUint(const std::string& node, const std::string& attributeName)
{
udev_device* udevDevice = getUdevDevice(node);
if (!udevDevice)
{
sf::err() << "Unable to get joystick attribute. "
<< "Could not get udev device for joystick " << node << std::endl;
return 0;
}
const char* attribute = getUdevAttribute(udevDevice, attributeName);
unsigned int value = 0;
if (attribute)
value = static_cast<unsigned int>(std::strtoul(attribute, NULL, 16));
udev_device_unref(udevDevice);
return value;
}
// Get vendor id for a joystick devnode
unsigned int getJoystickVendorId(const std::string& node)
{
unsigned int id = 0;
// First try using udev
id = getUdevAttributeUint(node, "ID_VENDOR_ID");
if (id)
return id;
// Fall back to using USB attribute
id = getUsbAttributeUint(node, "idVendor");
if (id)
return id;
sf::err() << "Failed to get vendor ID of joystick " << node << std::endl;
return 0;
}
// Get product id for a joystick devnode
unsigned int getJoystickProductId(const std::string& node)
{
unsigned int id = 0;
// First try using udev
id = getUdevAttributeUint(node, "ID_MODEL_ID");
if (id)
return id;
// Fall back to using USB attribute
id = getUsbAttributeUint(node, "idProduct");
if (id)
return id;
sf::err() << "Failed to get product ID of joystick " << node << std::endl;
return 0;
}
// Get the joystick name
std::string getJoystickName(const std::string& node)
{
// First try using ioctl with JSIOCGNAME
int fd = ::open(node.c_str(), O_RDONLY | O_NONBLOCK);
if (fd >= 0)
{
// Get the name
char name[128];
std::memset(name, 0, sizeof(name));
int result = ioctl(fd, JSIOCGNAME(sizeof(name)), name);
::close(fd);
if (result >= 0)
return std::string(name);
}
// Fall back to manual USB chain walk via udev
if (udevContext)
{
udev_device* udevDevice = getUdevDevice(node);
if (udevDevice)
{
const char* product = getUsbAttribute(udevDevice, "product");
udev_device_unref(udevDevice);
if (product)
return std::string(product);
}
}
sf::err() << "Unable to get name for joystick " << node << std::endl;
return std::string("Unknown Joystick");
}
}
@ -194,61 +369,86 @@ void JoystickImpl::initialize()
// Reset the array of plugged joysticks
std::fill(plugged, plugged + Joystick::Count, false);
udev* udevContext = udev_new();
if (!udevContext)
sf::err() << "Failed to create udev context, getting joystick attributes not available" << std::endl;
else
udevMonitor = udev_monitor_new_from_netlink(udevContext, "udev");
if (!udevMonitor)
{
err() << "Failed to create udev monitor, joystick connections and disconnections won't be notified" << std::endl;
}
else
{
int error = udev_monitor_filter_add_match_subsystem_devtype(udevMonitor, "input", NULL);
if (error < 0)
{
err() << "Failed to add udev monitor filter, joystick connections and disconnections won't be notified: " << error << std::endl;
udev_monitor_unref(udevMonitor);
udevMonitor = 0;
}
else
{
error = udev_monitor_enable_receiving(udevMonitor);
if (error < 0)
{
err() << "Failed to enable udev monitor, joystick connections and disconnections won't be notified: " << error << std::endl;
udev_monitor_unref(udevMonitor);
udevMonitor = 0;
}
}
}
// Do an initial scan
updatePluggedList();
// Create the inotify instance
notifyFd = inotify_init();
if (notifyFd < 0)
{
err() << "Failed to initialize inotify, joystick connections and disconnections won't be notified" << std::endl;
return;
}
// Watch nodes created and deleted in the /dev/input directory
inputFd = inotify_add_watch(notifyFd, "/dev/input", IN_CREATE | IN_DELETE);
if (inputFd < 0)
{
err() << "Failed to initialize inotify, joystick connections and disconnections won't be notified" << std::endl;
// No need to hang on to the inotify handle in this case
::close(notifyFd);
notifyFd = -1;
}
}
////////////////////////////////////////////////////////////
void JoystickImpl::cleanup()
{
// Stop watching the /dev/input directory
if (inputFd >= 0)
inotify_rm_watch(notifyFd, inputFd);
// Unreference the udev monitor to destroy it
if (udevMonitor)
{
udev_monitor_unref(udevMonitor);
udevMonitor = 0;
}
// Close the inotify file descriptor
if (notifyFd >= 0)
::close(notifyFd);
// Unreference the udev context to destroy it
if (udevContext)
{
udev_unref(udevContext);
udevContext = 0;
}
}
////////////////////////////////////////////////////////////
bool JoystickImpl::isConnected(unsigned int index)
{
// See if we can skip scanning if inotify is available
if (notifyFd < 0)
// See if we can skip scanning if udev monitor is available
if (!udevMonitor)
{
// inotify is not available, perform a scan every query
// udev monitor is not available, perform a scan every query
updatePluggedList();
}
else if (hasInotifyEvent())
else if (hasMonitorEvent())
{
// Check if new joysticks were added/removed since last update
// Don't bother decomposing and interpreting the filename, just do a full scan
updatePluggedList();
struct udev_device* udevDevice = udev_monitor_receive_device(udevMonitor);
// Flush all the pending events
if (lseek(notifyFd, 0, SEEK_END) < 0)
err() << "Failed to flush inotify of all pending joystick events." << std::endl;
// If we can get the specific device, we check that,
// otherwise just do a full scan if udevDevice == NULL
updatePluggedList(udevDevice);
if (udevDevice)
udev_device_unref(udevDevice);
}
// Then check if the joystick is connected
@ -271,15 +471,23 @@ bool JoystickImpl::open(unsigned int index)
ioctl(m_file, JSIOCGAXMAP, m_mapping);
// Get info
m_identification.name = getJoystickName(m_file, index);
m_identification.vendorId = getAttributeUint(index, "idVendor");
m_identification.productId = getAttributeUint(index, "idProduct");
m_identification.name = getJoystickName(name.str());
if (udevContext)
{
m_identification.vendorId = getJoystickVendorId(name.str());
m_identification.productId = getJoystickProductId(name.str());
}
// Reset the joystick state
m_state = JoystickState();
return true;
}
else
{
err() << "Failed to open joystick " << name.str() << ": " << errno << std::endl;
}
}
return false;
@ -290,6 +498,7 @@ bool JoystickImpl::open(unsigned int index)
void JoystickImpl::close()
{
::close(m_file);
m_file = -1;
}
@ -298,6 +507,9 @@ JoystickCaps JoystickImpl::getCapabilities() const
{
JoystickCaps caps;
if (m_file < 0)
return caps;
// Get the number of buttons
char buttonCount;
ioctl(m_file, JSIOCGBUTTONS, &buttonCount);
@ -340,9 +552,16 @@ Joystick::Identification JoystickImpl::getIdentification() const
////////////////////////////////////////////////////////////
JoystickState JoystickImpl::JoystickImpl::update()
{
if (m_file < 0)
{
m_state = JoystickState();
return m_state;
}
// pop events from the joystick file
js_event joyState;
while (read(m_file, &joyState, sizeof(joyState)) > 0)
int result = read(m_file, &joyState, sizeof(joyState));
while (result > 0)
{
switch (joyState.type & ~JS_EVENT_INIT)
{
@ -375,10 +594,18 @@ JoystickState JoystickImpl::JoystickImpl::update()
break;
}
}
result = read(m_file, &joyState, sizeof(joyState));
}
// Check the connection state of the joystick (read() fails with an error != EGAIN if it's no longer connected)
m_state.connected = (errno == EAGAIN);
// Check the connection state of the joystick
// read() returns -1 and errno != EGAIN if it's no longer connected
// We need to check the result of read() as well, since errno could
// have been previously set by some other function call that failed
// result can be either negative or 0 at this point
// If result is 0, assume the joystick is still connected
// If result is negative, check errno and disconnect if it is not EAGAIN
m_state.connected = (!result || (errno == EAGAIN));
return m_state;
}

View File

@ -28,9 +28,8 @@
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <linux/joystick.h>
#include <fcntl.h>
#include <string>
#include <SFML/Window/JoystickImpl.hpp>
#include <linux/input.h>
namespace sf