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 #ifndef __FreeBSD__
138  // We do this by checking for a js# directory inside the sysfs directory.
139  sprintf(path, "/sys/class/input/event%zd/device", ev_index);
140 
141  DIR *dir = opendir(path);
142  if (dir == nullptr) {
143  if (device_cat.is_debug()) {
144  device_cat.debug()
145  << "Error opening directory " << path << ": " << strerror(errno) << "\n";
146  }
147  return nullptr;
148  }
149 
150  dirent *entry = readdir(dir);
151  while (entry != nullptr) {
152  size_t js_index;
153  if (sscanf(entry->d_name, "js%zd", &js_index) == 1) {
154  // Yes, we fonud a corresponding js device. Try adding it.
155  closedir(dir);
156 
157  InputDevice *device = consider_add_js_device(js_index);
158  if (device != nullptr && device_cat.is_warning()) {
159  // This seemed to work. Display a warning to the user indicating
160  // that they might want to configure udev properly.
161  device_cat.warning()
162  << "/dev/input/event" << ev_index << " is not readable, some "
163  "features will be unavailable.\n";
164  }
165  _evdev_devices[ev_index] = device;
166  return device;
167  }
168  entry = readdir(dir);
169  }
170 
171  closedir(dir);
172 #endif
173 
174  return nullptr;
175 }
176 
177 /**
178  * Checks whether the joystick device with the given index is accessible, and
179  * if so, adds it. Returns the device if it was newly connected.
180  *
181  * This is only used on Linux as a fallback interface for when an evdev device
182  * cannot be accessed.
183  */
184 InputDevice *LinuxInputDeviceManager::
185 consider_add_js_device(size_t js_index) {
186  char path[64];
187  sprintf(path, "/dev/input/js%zd", js_index);
188 
189  if (access(path, R_OK) == 0) {
190  PT(LinuxJoystickDevice) device = new LinuxJoystickDevice(this, js_index);
191  if (device_cat.is_debug()) {
192  device_cat.debug()
193  << "Discovered joydev input device " << *device << "\n";
194  }
195  InputDevice *device_p = device.p();
196 
197  if (device->is_connected()) {
198  _connected_devices.add_device(std::move(device));
199  } else {
200  // Wait for activity on the device before it is considered connected.
201  _inactive_devices.add_device(std::move(device));
202  }
203  return device_p;
204  }
205 
206  return nullptr;
207 }
208 
209 /**
210  * Scans the "virtual" input devices on the system to check whether one with
211  * the given vendor and product ID exists.
212  */
213 bool LinuxInputDeviceManager::
214 has_virtual_device(unsigned short vendor_id, unsigned short product_id) const {
215  char path[294];
216  sprintf(path, "/sys/devices/virtual/input");
217 
218  DIR *dir = opendir(path);
219  if (dir != nullptr) {
220  dirent *entry = readdir(dir);
221  while (entry != nullptr) {
222  if (entry->d_name[0] != 'i') {
223  entry = readdir(dir);
224  continue;
225  }
226  FILE *f;
227 
228  char vendor[5] = {0};
229  sprintf(path, "/sys/devices/virtual/input/%s/id/vendor", entry->d_name);
230  f = fopen(path, "r");
231  if (f) {
232  fgets(vendor, sizeof(vendor), f);
233  fclose(f);
234  }
235 
236  char product[5] = {0};
237  sprintf(path, "/sys/devices/virtual/input/%s/id/product", entry->d_name);
238  f = fopen(path, "r");
239  if (f) {
240  fgets(product, sizeof(product), f);
241  fclose(f);
242  }
243 
244  if (vendor[0] && std::stoi(std::string(vendor), nullptr, 16) == (int)vendor_id &&
245  product[0] && std::stoi(std::string(product), nullptr, 16) == (int)product_id) {
246  closedir(dir);
247  return true;
248  }
249 
250  entry = readdir(dir);
251  }
252  closedir(dir);
253  }
254 
255  return false;
256 }
257 
258 /**
259  * Polls the system to see if there are any new devices. In some
260  * implementations this is a no-op.
261  */
262 void LinuxInputDeviceManager::
263 update() {
264  // Check for any devices that may be disconnected and need to be probed in
265  // order to see whether they have been reconnected.
266  InputDeviceSet inactive_devices;
267  {
268  LightMutexHolder holder(_lock);
269  inactive_devices = _inactive_devices;
270  }
271  for (size_t i = 0; i < inactive_devices.size(); ++i) {
272  InputDevice *device = inactive_devices[i];
273  if (device != nullptr && !device->is_connected()) {
274  device->poll();
275  }
276  }
277 
278  // We use inotify to tell us whether a device was added, removed, or has
279  // changed permissions to allow us to access it.
280  unsigned int avail = 0;
281  ioctl(_inotify_fd, FIONREAD, &avail);
282  if (avail == 0) {
283  return;
284  }
285 
286  char buffer[avail];
287  int n_read = read(_inotify_fd, buffer, avail);
288  if (n_read < 0) {
289  if (errno == EAGAIN || errno == EWOULDBLOCK) {
290  // No data available for now.
291 
292  } else {
293  device_cat.error() << "read: " << strerror(errno) << "\n";
294  }
295  return;
296  }
297 
298  LightMutexHolder holder(_lock);
299 
300  // Iterate over the events in the buffer.
301  bool removed_steam_virtual_device = false;
302  char *ptr = buffer;
303  char *end = buffer + avail;
304  while (ptr < end) {
305  inotify_event *event = (inotify_event *)ptr;
306 
307  std::string name(event->name);
308 
309  if (event->mask & IN_DELETE) {
310  // The device was deleted. If we have it, remove it.
311 
312  size_t index;
313  if (sscanf(event->name, "event%zd", &index) == 1) {
314  // Check if we have this evdev device. If so, disconnect it.
315  if (index < _evdev_devices.size()) {
316  PT(InputDevice) device = _evdev_devices[index];
317  if (device != nullptr) {
318  device->set_connected(false);
319  _evdev_devices[index] = nullptr;
320  _inactive_devices.remove_device(device);
321  if (_connected_devices.remove_device(device)) {
322  throw_event("disconnect-device", device.p());
323  }
324 
325  if (device_cat.is_debug()) {
326  device_cat.debug()
327  << "Removed input device " << *device << "\n";
328  }
329 
330  // Check for Steam virtual device; see comment below.
331  if (device->get_vendor_id() == 0x28de &&
332  device->get_product_id() == 0x11ff) {
333  removed_steam_virtual_device = true;
334  }
335  }
336  }
337  }
338 
339  } else if (event->mask & (IN_CREATE | IN_ATTRIB)) {
340  // The device was created, or it was chmodded to be accessible. We need
341  // to check for the latter since it seems that the device may get the
342  // IN_CREATE event before the driver gets the permissions set properly.
343 
344  size_t index;
345  if (sscanf(event->name, "event%zd", &index) == 1) {
346  InputDevice *device = consider_add_evdev_device(index);
347  if (device != nullptr && device->is_connected()) {
348  throw_event("connect-device", device);
349  }
350  }
351  }
352 
353  ptr += sizeof(inotify_event) + event->len;
354  }
355 
356  // If the Steam virtual device was just disconnected, the user may have just
357  // shut down Steam, and we need to reactivate the real Steam Controller
358  // device that was previously suppressed by Steam.
359  if (removed_steam_virtual_device) {
360  inactive_devices = _inactive_devices;
361 
362  for (size_t i = 0; i < inactive_devices.size(); ++i) {
363  InputDevice *device = inactive_devices[i];
364  if (device != nullptr && device->is_of_type(EvdevInputDevice::get_class_type())) {
365  PT(EvdevInputDevice) evdev_device = (EvdevInputDevice *)device;
366  if (evdev_device->reactivate_steam_controller()) {
367  _inactive_devices.remove_device(device);
368  _connected_devices.add_device(device);
369  throw_event("connect-device", device);
370  }
371  }
372  }
373  }
374 }
375 
376 #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.