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 
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
Manages a list of InputDevice objects, as returned by various InputDeviceManager methods.
size_t size() const
Returns the number of devices in the collection.
This is a structure representing a single input device.
Definition: inputDevice.h:53
is_connected
Returns true if the device is still connected and able to receive data, false otherwise.
Definition: inputDevice.h:244
get_product_id
Returns a string containing the USB product ID of the device, if this information is known.
Definition: inputDevice.h:240
get_vendor_id
Returns a string containing the USB vendor ID of the device, if this information is known.
Definition: inputDevice.h:237
void set_connected(bool connected)
Called to indicate that the device has been disconnected or connected from its host.
Definition: inputDevice.I:383
void poll()
Polls the input device for new activity, to ensure it contains the latest events.
Definition: inputDevice.cxx:48
Similar to MutexHolder, but for a light mutex.
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition: typedObject.I:28
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.