Panda3D

linuxInputDeviceManager.cxx
Go to the documentation of this file.
1 /**
2 * PANDA 3D SOFTWARE
3 * Copyright (c) Carnegie Mellon University. All rights reserved.
4 *
5 * All use of this software is subject to the terms of the revised BSD
6 * license. You should have received a copy of this license along
7 * with this source code in a file named "LICENSE."
8 *
9 * @file linuxInputDeviceManager.cxx
10 * @author rdb
11 * @date 2018-01-25
12 */
13
14 #include "linuxInputDeviceManager.h"
15 #include "throw_event.h"
16
17 #ifdef PHAVE_LINUX_INPUT_H
18
19 #include "evdevInputDevice.h"
20 #include "linuxJoystickDevice.h"
21
22 #include <dirent.h>
23 #include <fcntl.h>
24 #include <sys/inotify.h>
25 #include <sys/ioctl.h>
26
27 /**
28 * Initializes the device manager by scanning which devices are currently
29 * connected and setting up any platform-dependent structures necessary for
30 * listening for future device connect events.
31 */
32 LinuxInputDeviceManager::
33 LinuxInputDeviceManager() {
34 // Use inotify to watch /dev/input for hotplugging of devices.
35 _inotify_fd = inotify_init();
36 fcntl(_inotify_fd, F_SETFL, O_NONBLOCK);
37 fcntl(_inotify_fd, F_SETFD, FD_CLOEXEC);
38
39 if (_inotify_fd < 0) {
40 device_cat.error()
41 << "Error initializing inotify: " << strerror(errno) << "\n";
42
43 } else if (inotify_add_watch(_inotify_fd, "/dev/input", IN_CREATE | IN_ATTRIB | IN_DELETE) < 0) {
44 device_cat.error()
45 << "Error adding inotify watch on /dev/input: " << strerror(errno) << "\n";
46 }
47
48 // Scan /dev/input for a list of input devices.
49 DIR *dir = opendir("/dev/input");
50 if (dir) {
51 std::vector<size_t> indices;
52 dirent *entry = readdir(dir);
53 while (entry != nullptr) {
54 size_t index;
55 if (entry->d_type == DT_CHR && sscanf(entry->d_name, "event%zd", &index) == 1) {
56 indices.push_back(index);
57 }
58 entry = readdir(dir);
59 }
60 closedir(dir);
61
62 // We'll want to sort the devices by index, since the order may be
63 // meaningful (eg. for the Xbox wireless receiver).
64 if (indices.empty()) {
65 return;
66 }
67 std::sort(indices.begin(), indices.end());
68 _evdev_devices.resize(indices.back() + 1, nullptr);
69
70 for (size_t index : indices) {
71 consider_add_evdev_device(index);
72 }
73 } else {
74 device_cat.error()
75 << "Error opening directory /dev/input: " << strerror(errno) << "\n";
76 return;
77 }
78 }
79
80 /**
81 * Closes any resources that the device manager was using to listen for events.
82 */
83 LinuxInputDeviceManager::
84 ~LinuxInputDeviceManager() {
85 if (_inotify_fd >= 0) {
86 close(_inotify_fd);
87 _inotify_fd = -1;
88 }
89 }
90
91 /**
92 * Checks whether the event device with the given index is accessible, and if
93 * so, adds it. Returns the device if it was newly connected.
94 *
95 * This is the preferred interface for input devices on Linux.
96 */
97 InputDevice *LinuxInputDeviceManager::
98 consider_add_evdev_device(size_t ev_index) {
99 if (ev_index < _evdev_devices.size()) {
100 if (_evdev_devices[ev_index] != nullptr) {
101 // We already have this device. FIXME: probe it and add it to the
102 // list of connected devices?
103 return nullptr;
104 }
105 } else {
106 // Make room to store this index.
107 _evdev_devices.resize(ev_index + 1, nullptr);
108 }
109
110 // Check if we can directly read the event device.
111 char path[64];
112 sprintf(path, "/dev/input/event%zd", ev_index);
113
114 if (access(path, R_OK) == 0) {
115 PT(InputDevice) device = new EvdevInputDevice(this, ev_index);
116 if (device_cat.is_debug()) {
117 device_cat.debug()
118 << "Discovered evdev input device " << *device << "\n";
119 }
120
121 _evdev_devices[ev_index] = device;
122
123 if (device->is_connected()) {
124 _connected_devices.add_device(std::move(device));
125 } else {
126 // Wait for activity on the device before it is considered connected.
127 _inactive_devices.add_device(std::move(device));
128 }
129 return _evdev_devices[ev_index];
130 }
131
132 // Nope. The permissions haven't been configured to allow it. Check if this
133 // corresponds to a /dev/input/jsX interface, which has a better chance of
134 // having read permissions set, but doesn't export all of the features
135 // (notably, force feedback).
136
137 // We do this by checking for a js# directory inside the sysfs directory.
138 sprintf(path, "/sys/class/input/event%zd/device", ev_index);
139
140 DIR *dir = opendir(path);
141 if (dir == nullptr) {
142 if (device_cat.is_debug()) {
143 device_cat.debug()
144 << "Error opening directory " << path << ": " << strerror(errno) << "\n";
145 }
146 return nullptr;
147 }
148
149 dirent *entry = readdir(dir);
150 while (entry != nullptr) {
151 size_t js_index;
152 if (sscanf(entry->d_name, "js%zd", &js_index) == 1) {
153 // Yes, we fonud a corresponding js device. Try adding it.
154 closedir(dir);
155
156 InputDevice *device = consider_add_js_device(js_index);
157 if (device != nullptr && device_cat.is_warning()) {
158 // This seemed to work. Display a warning to the user indicating
159 // that they might want to configure udev properly.
160 device_cat.warning()
161 << "/dev/input/event" << ev_index << " is not readable, some "
162 "features will be unavailable.\n";
163 }
164 _evdev_devices[ev_index] = device;
165 return device;
166 }
167 entry = readdir(dir);
168 }
169
170 closedir(dir);
171 return nullptr;
172 }
173
174 /**
175 * Checks whether the joystick device with the given index is accessible, and
176 * if so, adds it. Returns the device if it was newly connected.
177 *
178 * This is only used on Linux as a fallback interface for when an evdev device
179 * cannot be accessed.
180 */
181 InputDevice *LinuxInputDeviceManager::
182 consider_add_js_device(size_t js_index) {
183 char path[64];
184 sprintf(path, "/dev/input/js%zd", js_index);
185
186 if (access(path, R_OK) == 0) {
187 PT(LinuxJoystickDevice) device = new LinuxJoystickDevice(this, js_index);
188 if (device_cat.is_debug()) {
189 device_cat.debug()
190 << "Discovered joydev input device " << *device << "\n";
191 }
192 InputDevice *device_p = device.p();
193
194 if (device->is_connected()) {
195 _connected_devices.add_device(std::move(device));
196 } else {
197 // Wait for activity on the device before it is considered connected.
198 _inactive_devices.add_device(std::move(device));
199 }
200 return device_p;
201 }
202
203 return nullptr;
204 }
205
206 /**
207 * Scans the "virtual" input devices on the system to check whether one with
208 * the given vendor and product ID exists.
209 */
210 bool LinuxInputDeviceManager::
211 has_virtual_device(unsigned short vendor_id, unsigned short product_id) const {
212 char path[294];
213 sprintf(path, "/sys/devices/virtual/input");
214
215 DIR *dir = opendir(path);
216 if (dir != nullptr) {
217 dirent *entry = readdir(dir);
218 while (entry != nullptr) {
219 if (entry->d_name[0] != 'i') {
220 entry = readdir(dir);
221 continue;
222 }
223 FILE *f;
224
225 char vendor[5] = {0};
226 sprintf(path, "/sys/devices/virtual/input/%s/id/vendor", entry->d_name);
227 f = fopen(path, "r");
228 if (f) {
229 fgets(vendor, sizeof(vendor), f);
230 fclose(f);
231 }
232
233 char product[5] = {0};
234 sprintf(path, "/sys/devices/virtual/input/%s/id/product", entry->d_name);
235 f = fopen(path, "r");
236 if (f) {
237 fgets(product, sizeof(product), f);
238 fclose(f);
239 }
240
241 if (vendor[0] && std::stoi(std::string(vendor), nullptr, 16) == (int)vendor_id &&
242 product[0] && std::stoi(std::string(product), nullptr, 16) == (int)product_id) {
243 closedir(dir);
244 return true;
245 }
246
247 entry = readdir(dir);
248 }
249 closedir(dir);
250 }
251
252 return false;
253 }
254
255 /**
256 * Polls the system to see if there are any new devices. In some
257 * implementations this is a no-op.
258 */
259 void LinuxInputDeviceManager::
260 update() {
261 // Check for any devices that may be disconnected and need to be probed in
262 // order to see whether they have been reconnected.
263 InputDeviceSet inactive_devices;
264 {
265 LightMutexHolder holder(_lock);
266 inactive_devices = _inactive_devices;
267 }
268 for (size_t i = 0; i < inactive_devices.size(); ++i) {
269 InputDevice *device = inactive_devices[i];
270 if (device != nullptr && !device->is_connected()) {
271 device->poll();
272 }
273 }
274
275 // We use inotify to tell us whether a device was added, removed, or has
276 // changed permissions to allow us to access it.
277 unsigned int avail = 0;
278 ioctl(_inotify_fd, FIONREAD, &avail);
279 if (avail == 0) {
280 return;
281 }
282
283 char buffer[avail];
284 int n_read = read(_inotify_fd, buffer, avail);
285 if (n_read < 0) {
286 if (errno == EAGAIN || errno == EWOULDBLOCK) {
287 // No data available for now.
288
289 } else {
290 device_cat.error() << "read: " << strerror(errno) << "\n";
291 }
292 return;
293 }
294
295 LightMutexHolder holder(_lock);
296
297 // Iterate over the events in the buffer.
298 bool removed_steam_virtual_device = false;
299 char *ptr = buffer;
300 char *end = buffer + avail;
301 while (ptr < end) {
302 inotify_event *event = (inotify_event *)ptr;
303
304 std::string name(event->name);
305
306 if (event->mask & IN_DELETE) {
307 // The device was deleted. If we have it, remove it.
308
309 size_t index;
310 if (sscanf(event->name, "event%zd", &index) == 1) {
311 // Check if we have this evdev device. If so, disconnect it.
312 if (index < _evdev_devices.size()) {
313 PT(InputDevice) device = _evdev_devices[index];
314 if (device != nullptr) {
315 device->set_connected(false);
316 _evdev_devices[index] = nullptr;
317 _inactive_devices.remove_device(device);
318 if (_connected_devices.remove_device(device)) {
319 throw_event("disconnect-device", device.p());
320 }
321
322 if (device_cat.is_debug()) {
323 device_cat.debug()
324 << "Removed input device " << *device << "\n";
325 }
326
327 // Check for Steam virtual device; see comment below.
328 if (device->get_vendor_id() == 0x28de &&
329 device->get_product_id() == 0x11ff) {
330 removed_steam_virtual_device = true;
331 }
332 }
333 }
334 }
335
336 } else if (event->mask & (IN_CREATE | IN_ATTRIB)) {
337 // The device was created, or it was chmodded to be accessible. We need
338 // to check for the latter since it seems that the device may get the
339 // IN_CREATE event before the driver gets the permissions set properly.
340
341 size_t index;
342 if (sscanf(event->name, "event%zd", &index) == 1) {
343 InputDevice *device = consider_add_evdev_device(index);
344 if (device != nullptr && device->is_connected()) {
345 throw_event("connect-device", device);
346 }
347 }
348 }
349
350 ptr += sizeof(inotify_event) + event->len;
351 }
352
353 // If the Steam virtual device was just disconnected, the user may have just
354 // shut down Steam, and we need to reactivate the real Steam Controller
355 // device that was previously suppressed by Steam.
356 if (removed_steam_virtual_device) {
357 inactive_devices = _inactive_devices;
358
359 for (size_t i = 0; i < inactive_devices.size(); ++i) {
360 InputDevice *device = inactive_devices[i];
361 if (device != nullptr && device->is_of_type(EvdevInputDevice::get_class_type())) {
362 PT(EvdevInputDevice) evdev_device = (EvdevInputDevice *)device;
363 if (evdev_device->reactivate_steam_controller()) {
364 _inactive_devices.remove_device(device);
365 _connected_devices.add_device(device);
366 throw_event("connect-device", device);
367 }
368 }
369 }
370 }
371 }
372
373 #endif // PHAVE_LINUX_INPUT_H
InputDeviceSet
Manages a list of InputDevice objects, as returned by various InputDeviceManager methods.
Definition: inputDeviceSet.h:26
InputDeviceSet::size
size_t size() const
Returns the number of devices in the collection.
Definition: inputDeviceSet.I:34
InputDevice
This is a structure representing a single input device.
Definition: inputDevice.h:53
InputDevice::is_connected
is_connected
Returns true if the device is still connected and able to receive data, false otherwise.
Definition: inputDevice.h:244
InputDevice::get_product_id
get_product_id
Returns a string containing the USB product ID of the device, if this information is known.
Definition: inputDevice.h:240
InputDevice::get_vendor_id
get_vendor_id
Returns a string containing the USB vendor ID of the device, if this information is known.
Definition: inputDevice.h:237
InputDevice::set_connected
void set_connected(bool connected)
Called to indicate that the device has been disconnected or connected from its host.
Definition: inputDevice.I:383
InputDevice::poll
void poll()
Polls the input device for new activity, to ensure it contains the latest events.
Definition: inputDevice.cxx:48
LightMutexHolder
Similar to MutexHolder, but for a light mutex.
Definition: lightMutexHolder.h:25
TypedObject::is_of_type
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition: typedObject.I:28
evdevInputDevice.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
linuxInputDeviceManager.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
linuxJoystickDevice.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
throw_event.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.