/** * PANDA 3D SOFTWARE * Copyright (c) Carnegie Mellon University. All rights reserved. * * All use of this software is subject to the terms of the revised BSD * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * * @file linuxInputDeviceManager.cxx * @author rdb * @date 2018-01-25 */ #include "inputDeviceManager.h" #include "throw_event.h" #ifdef PHAVE_LINUX_INPUT_H #include "evdevInputDevice.h" #include "linuxJoystickDevice.h" #include #include #include #include /** * Initializes the device manager by scanning which devices are currently * connected and setting up any platform-dependent structures necessary for * listening for future device connect events. */ LinuxInputDeviceManager:: LinuxInputDeviceManager() { // Use inotify to watch /dev/input for hotplugging of devices. _inotify_fd = inotify_init(); fcntl(_inotify_fd, F_SETFL, O_NONBLOCK); fcntl(_inotify_fd, F_SETFD, FD_CLOEXEC); if (_inotify_fd < 0) { device_cat.error() << "Error initializing inotify: " << strerror(errno) << "\n"; } else if (inotify_add_watch(_inotify_fd, "/dev/input", IN_CREATE | IN_ATTRIB | IN_DELETE) < 0) { device_cat.error() << "Error adding inotify watch on /dev/input: " << strerror(errno) << "\n"; } // Scan /dev/input for a list of input devices. DIR *dir = opendir("/dev/input"); if (dir) { std::vector indices; dirent *entry = readdir(dir); while (entry != nullptr) { size_t index; if (entry->d_type == DT_CHR && sscanf(entry->d_name, "event%zd", &index) == 1) { indices.push_back(index); } entry = readdir(dir); } closedir(dir); // We'll want to sort the devices by index, since the order may be // meaningful (eg. for the Xbox wireless receiver). std::sort(indices.begin(), indices.end()); _evdev_devices.resize(indices.back() + 1, nullptr); for (size_t index : indices) { consider_add_evdev_device(index); } } else { device_cat.error() << "Error opening directory /dev/input: " << strerror(errno) << "\n"; return; } } /** * Closes any resources that the device manager was using to listen for events. */ LinuxInputDeviceManager:: ~LinuxInputDeviceManager() { if (_inotify_fd >= 0) { close(_inotify_fd); _inotify_fd = -1; } } /** * Checks whether the event device with the given index is accessible, and if * so, adds it. Returns the device if it was newly connected. * * This is the preferred interface for input devices on Linux. */ InputDevice *LinuxInputDeviceManager:: consider_add_evdev_device(size_t ev_index) { if (ev_index < _evdev_devices.size()) { if (_evdev_devices[ev_index] != nullptr) { // We already have this device. FIXME: probe it and add it to the // list of connected devices? return nullptr; } } else { // Make room to store this index. _evdev_devices.resize(ev_index + 1, nullptr); } // Check if we can directly read the event device. char path[64]; sprintf(path, "/dev/input/event%zd", ev_index); if (access(path, R_OK) == 0) { PT(InputDevice) device = new EvdevInputDevice(this, ev_index); if (device_cat.is_debug()) { device_cat.debug() << "Discovered evdev input device " << *device << "\n"; } _evdev_devices[ev_index] = device; if (device->is_connected()) { _connected_devices.add_device(std::move(device)); } else { // Wait for activity on the device before it is considered connected. _inactive_devices.add_device(std::move(device)); } return _evdev_devices[ev_index]; } // Nope. The permissions haven't been configured to allow it. Check if this // corresponds to a /dev/input/jsX interface, which has a better chance of // having read permissions set, but doesn't export all of the features // (notably, force feedback). // We do this by checking for a js# directory inside the sysfs directory. sprintf(path, "/sys/class/input/event%zd/device", ev_index); DIR *dir = opendir(path); if (dir == nullptr) { if (device_cat.is_debug()) { device_cat.debug() << "Error opening directory " << path << ": " << strerror(errno) << "\n"; } return nullptr; } dirent *entry = readdir(dir); while (entry != nullptr) { size_t js_index; if (sscanf(entry->d_name, "js%zd", &js_index) == 1) { // Yes, we fonud a corresponding js device. Try adding it. closedir(dir); InputDevice *device = consider_add_js_device(js_index); if (device != nullptr && device_cat.is_warning()) { // This seemed to work. Display a warning to the user indicating // that they might want to configure udev properly. device_cat.warning() << "/dev/input/event" << ev_index << " is not readable, some " "features will be unavailable.\n"; } _evdev_devices[ev_index] = device; return device; } entry = readdir(dir); } closedir(dir); return nullptr; } /** * Checks whether the joystick device with the given index is accessible, and * if so, adds it. Returns the device if it was newly connected. * * This is only used on Linux as a fallback interface for when an evdev device * cannot be accessed. */ InputDevice *LinuxInputDeviceManager:: consider_add_js_device(size_t js_index) { char path[64]; sprintf(path, "/dev/input/js%zd", js_index); if (access(path, R_OK) == 0) { PT(LinuxJoystickDevice) device = new LinuxJoystickDevice(this, js_index); if (device_cat.is_debug()) { device_cat.debug() << "Discovered joydev input device " << *device << "\n"; } InputDevice *device_p = device.p(); if (device->is_connected()) { _connected_devices.add_device(std::move(device)); } else { // Wait for activity on the device before it is considered connected. _inactive_devices.add_device(std::move(device)); } return device_p; } return nullptr; } /** * Polls the system to see if there are any new devices. In some * implementations this is a no-op. */ void LinuxInputDeviceManager:: update() { // Check for any devices that may be disconnected and need to be probed in // order to see whether they have been reconnected. InputDeviceSet inactive_devices; { LightMutexHolder holder(_lock); inactive_devices = _inactive_devices; } for (size_t i = 0; i < inactive_devices.size(); ++i) { InputDevice *device = inactive_devices[i]; if (device != nullptr && !device->is_connected()) { device->poll(); } } // We use inotify to tell us whether a device was added, removed, or has // changed permissions to allow us to access it. unsigned int avail = 0; ioctl(_inotify_fd, FIONREAD, &avail); if (avail == 0) { return; } char buffer[avail]; int n_read = read(_inotify_fd, buffer, avail); if (n_read < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // No data available for now. } else { device_cat.error() << "read: " << strerror(errno) << "\n"; } return; } LightMutexHolder holder(_lock); // Iterate over the events in the buffer. char *ptr = buffer; char *end = buffer + avail; while (ptr < end) { inotify_event *event = (inotify_event *)ptr; std::string name(event->name); if (event->mask & IN_DELETE) { // The device was deleted. If we have it, remove it. size_t index; if (sscanf(event->name, "event%zd", &index) == 1) { // Check if we have this evdev device. If so, disconnect it. if (index < _evdev_devices.size()) { PT(InputDevice) device = _evdev_devices[index]; if (device != nullptr) { device->set_connected(false); _evdev_devices[index] = nullptr; _inactive_devices.remove_device(device); if (_connected_devices.remove_device(device)) { throw_event("disconnect-device", device.p()); } if (device_cat.is_debug()) { device_cat.debug() << "Removed input device " << *device << "\n"; } } } } } else if (event->mask & (IN_CREATE | IN_ATTRIB)) { // The device was created, or it was chmodded to be accessible. We need // to check for the latter since it seems that the device may get the // IN_CREATE event before the driver gets the permissions set properly. size_t index; if (sscanf(event->name, "event%zd", &index) == 1) { InputDevice *device = consider_add_evdev_device(index); if (device != nullptr && device->is_connected()) { throw_event("connect-device", device); } } } ptr += sizeof(inotify_event) + event->len; } } #endif // PHAVE_LINUX_INPUT_H