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
LightMutexHolder
Similar to MutexHolder, but for a light mutex.
Definition: lightMutexHolder.h:25
throw_event.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
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_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
This is a structure representing a single input device.
Definition: inputDevice.h:53
InputDevice::poll
void poll()
Polls the input device for new activity, to ensure it contains the latest events.
Definition: inputDevice.cxx:48
linuxInputDeviceManager.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
InputDeviceSet::size
size_t size() const
Returns the number of devices in the collection.
Definition: inputDeviceSet.I:34
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
InputDeviceSet
Manages a list of InputDevice objects, as returned by various InputDeviceManager methods.
Definition: inputDeviceSet.h:26
evdevInputDevice.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
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
linuxJoystickDevice.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
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