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