1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23/*
24 * To list the properties of a device, try something like:
25 * udevadm info -a -n snd/hwC0D0 (for a sound card)
26 * udevadm info --query=all -n input/event3 (for a keyboard, mouse, etc)
27 * udevadm info --query=property -n input/event2
28 */
29#include "SDL_udev.h"
30
31#ifdef SDL_USE_LIBUDEV
32
33#include <linux/input.h>
34#include <sys/stat.h>
35
36#include "SDL_evdev_capabilities.h"
37#include "../unix/SDL_poll.h"
38
39static const char *SDL_UDEV_LIBS[] = { "libudev.so.1", "libudev.so.0" };
40
41static SDL_UDEV_PrivateData *_this = NULL;
42
43static bool SDL_UDEV_load_sym(const char *fn, void **addr);
44static bool SDL_UDEV_load_syms(void);
45static bool SDL_UDEV_hotplug_update_available(void);
46static void get_caps(struct udev_device *dev, struct udev_device *pdev, const char *attr, unsigned long *bitmask, size_t bitmask_len);
47static int guess_device_class(struct udev_device *dev);
48static int device_class(struct udev_device *dev);
49static void device_event(SDL_UDEV_deviceevent type, struct udev_device *dev);
50
51static bool SDL_UDEV_load_sym(const char *fn, void **addr)
52{
53 *addr = SDL_LoadFunction(_this->udev_handle, fn);
54 if (!*addr) {
55 // Don't call SDL_SetError(): SDL_LoadFunction already did.
56 return false;
57 }
58
59 return true;
60}
61
62static bool SDL_UDEV_load_syms(void)
63{
64/* cast funcs to char* first, to please GCC's strict aliasing rules. */
65#define SDL_UDEV_SYM(x) \
66 if (!SDL_UDEV_load_sym(#x, (void **)(char *)&_this->syms.x)) \
67 return false
68
69 SDL_UDEV_SYM(udev_device_get_action);
70 SDL_UDEV_SYM(udev_device_get_devnode);
71 SDL_UDEV_SYM(udev_device_get_syspath);
72 SDL_UDEV_SYM(udev_device_get_subsystem);
73 SDL_UDEV_SYM(udev_device_get_parent_with_subsystem_devtype);
74 SDL_UDEV_SYM(udev_device_get_property_value);
75 SDL_UDEV_SYM(udev_device_get_sysattr_value);
76 SDL_UDEV_SYM(udev_device_new_from_syspath);
77 SDL_UDEV_SYM(udev_device_unref);
78 SDL_UDEV_SYM(udev_enumerate_add_match_property);
79 SDL_UDEV_SYM(udev_enumerate_add_match_subsystem);
80 SDL_UDEV_SYM(udev_enumerate_get_list_entry);
81 SDL_UDEV_SYM(udev_enumerate_new);
82 SDL_UDEV_SYM(udev_enumerate_scan_devices);
83 SDL_UDEV_SYM(udev_enumerate_unref);
84 SDL_UDEV_SYM(udev_list_entry_get_name);
85 SDL_UDEV_SYM(udev_list_entry_get_next);
86 SDL_UDEV_SYM(udev_monitor_enable_receiving);
87 SDL_UDEV_SYM(udev_monitor_filter_add_match_subsystem_devtype);
88 SDL_UDEV_SYM(udev_monitor_get_fd);
89 SDL_UDEV_SYM(udev_monitor_new_from_netlink);
90 SDL_UDEV_SYM(udev_monitor_receive_device);
91 SDL_UDEV_SYM(udev_monitor_unref);
92 SDL_UDEV_SYM(udev_new);
93 SDL_UDEV_SYM(udev_unref);
94 SDL_UDEV_SYM(udev_device_new_from_devnum);
95 SDL_UDEV_SYM(udev_device_get_devnum);
96#undef SDL_UDEV_SYM
97
98 return true;
99}
100
101static bool SDL_UDEV_hotplug_update_available(void)
102{
103 if (_this->udev_mon) {
104 const int fd = _this->syms.udev_monitor_get_fd(_this->udev_mon);
105 if (SDL_IOReady(fd, SDL_IOR_READ, 0)) {
106 return true;
107 }
108 }
109 return false;
110}
111
112bool SDL_UDEV_Init(void)
113{
114 if (!_this) {
115 _this = (SDL_UDEV_PrivateData *)SDL_calloc(1, sizeof(*_this));
116 if (!_this) {
117 return false;
118 }
119
120 if (!SDL_UDEV_LoadLibrary()) {
121 SDL_UDEV_Quit();
122 return false;
123 }
124
125 /* Set up udev monitoring
126 * Listen for input devices (mouse, keyboard, joystick, etc) and sound devices
127 */
128
129 _this->udev = _this->syms.udev_new();
130 if (!_this->udev) {
131 SDL_UDEV_Quit();
132 return SDL_SetError("udev_new() failed");
133 }
134
135 _this->udev_mon = _this->syms.udev_monitor_new_from_netlink(_this->udev, "udev");
136 if (!_this->udev_mon) {
137 SDL_UDEV_Quit();
138 return SDL_SetError("udev_monitor_new_from_netlink() failed");
139 }
140
141 _this->syms.udev_monitor_filter_add_match_subsystem_devtype(_this->udev_mon, "input", NULL);
142 _this->syms.udev_monitor_filter_add_match_subsystem_devtype(_this->udev_mon, "sound", NULL);
143 _this->syms.udev_monitor_filter_add_match_subsystem_devtype(_this->udev_mon, "video4linux", NULL);
144 _this->syms.udev_monitor_enable_receiving(_this->udev_mon);
145
146 // Do an initial scan of existing devices
147 SDL_UDEV_Scan();
148 }
149
150 _this->ref_count += 1;
151
152 return true;
153}
154
155void SDL_UDEV_Quit(void)
156{
157 if (!_this) {
158 return;
159 }
160
161 _this->ref_count -= 1;
162
163 if (_this->ref_count < 1) {
164
165 if (_this->udev_mon) {
166 _this->syms.udev_monitor_unref(_this->udev_mon);
167 _this->udev_mon = NULL;
168 }
169 if (_this->udev) {
170 _this->syms.udev_unref(_this->udev);
171 _this->udev = NULL;
172 }
173
174 // Remove existing devices
175 while (_this->first) {
176 SDL_UDEV_CallbackList *item = _this->first;
177 _this->first = _this->first->next;
178 SDL_free(item);
179 }
180
181 SDL_UDEV_UnloadLibrary();
182 SDL_free(_this);
183 _this = NULL;
184 }
185}
186
187bool SDL_UDEV_Scan(void)
188{
189 struct udev_enumerate *enumerate = NULL;
190 struct udev_list_entry *devs = NULL;
191 struct udev_list_entry *item = NULL;
192
193 if (!_this) {
194 return true;
195 }
196
197 enumerate = _this->syms.udev_enumerate_new(_this->udev);
198 if (!enumerate) {
199 SDL_UDEV_Quit();
200 return SDL_SetError("udev_enumerate_new() failed");
201 }
202
203 _this->syms.udev_enumerate_add_match_subsystem(enumerate, "input");
204 _this->syms.udev_enumerate_add_match_subsystem(enumerate, "sound");
205 _this->syms.udev_enumerate_add_match_subsystem(enumerate, "video4linux");
206
207 _this->syms.udev_enumerate_scan_devices(enumerate);
208 devs = _this->syms.udev_enumerate_get_list_entry(enumerate);
209 for (item = devs; item; item = _this->syms.udev_list_entry_get_next(item)) {
210 const char *path = _this->syms.udev_list_entry_get_name(item);
211 struct udev_device *dev = _this->syms.udev_device_new_from_syspath(_this->udev, path);
212 if (dev) {
213 device_event(SDL_UDEV_DEVICEADDED, dev);
214 _this->syms.udev_device_unref(dev);
215 }
216 }
217
218 _this->syms.udev_enumerate_unref(enumerate);
219 return true;
220}
221
222bool SDL_UDEV_GetProductInfo(const char *device_path, Uint16 *vendor, Uint16 *product, Uint16 *version, int *class)
223{
224 struct stat statbuf;
225 char type;
226 struct udev_device *dev;
227 const char* val;
228 int class_temp;
229
230 if (!_this) {
231 return false;
232 }
233
234 if (stat(device_path, &statbuf) == -1) {
235 return false;
236 }
237
238 if (S_ISBLK(statbuf.st_mode)) {
239 type = 'b';
240 }
241 else if (S_ISCHR(statbuf.st_mode)) {
242 type = 'c';
243 }
244 else {
245 return false;
246 }
247
248 dev = _this->syms.udev_device_new_from_devnum(_this->udev, type, statbuf.st_rdev);
249
250 if (!dev) {
251 return false;
252 }
253
254 val = _this->syms.udev_device_get_property_value(dev, "ID_VENDOR_ID");
255 if (val) {
256 *vendor = (Uint16)SDL_strtol(val, NULL, 16);
257 }
258
259 val = _this->syms.udev_device_get_property_value(dev, "ID_MODEL_ID");
260 if (val) {
261 *product = (Uint16)SDL_strtol(val, NULL, 16);
262 }
263
264 val = _this->syms.udev_device_get_property_value(dev, "ID_REVISION");
265 if (val) {
266 *version = (Uint16)SDL_strtol(val, NULL, 16);
267 }
268
269 class_temp = device_class(dev);
270 if (class_temp) {
271 *class = class_temp;
272 }
273
274 _this->syms.udev_device_unref(dev);
275
276 return true;
277}
278
279void SDL_UDEV_UnloadLibrary(void)
280{
281 if (!_this) {
282 return;
283 }
284
285 if (_this->udev_handle) {
286 SDL_UnloadObject(_this->udev_handle);
287 _this->udev_handle = NULL;
288 }
289}
290
291bool SDL_UDEV_LoadLibrary(void)
292{
293 bool result = true;
294
295 if (!_this) {
296 return SDL_SetError("UDEV not initialized");
297 }
298
299 // See if there is a udev library already loaded
300 if (SDL_UDEV_load_syms()) {
301 return true;
302 }
303
304#ifdef SDL_UDEV_DYNAMIC
305 // Check for the build environment's libudev first
306 if (!_this->udev_handle) {
307 _this->udev_handle = SDL_LoadObject(SDL_UDEV_DYNAMIC);
308 if (_this->udev_handle) {
309 result = SDL_UDEV_load_syms();
310 if (!result) {
311 SDL_UDEV_UnloadLibrary();
312 }
313 }
314 }
315#endif
316
317 if (!_this->udev_handle) {
318 for (int i = 0; i < SDL_arraysize(SDL_UDEV_LIBS); i++) {
319 _this->udev_handle = SDL_LoadObject(SDL_UDEV_LIBS[i]);
320 if (_this->udev_handle) {
321 result = SDL_UDEV_load_syms();
322 if (!result) {
323 SDL_UDEV_UnloadLibrary();
324 } else {
325 break;
326 }
327 }
328 }
329
330 if (!_this->udev_handle) {
331 result = false;
332 // Don't call SDL_SetError(): SDL_LoadObject already did.
333 }
334 }
335
336 return result;
337}
338
339static void get_caps(struct udev_device *dev, struct udev_device *pdev, const char *attr, unsigned long *bitmask, size_t bitmask_len)
340{
341 const char *value;
342 char text[4096];
343 char *word;
344 int i;
345 unsigned long v;
346
347 SDL_memset(bitmask, 0, bitmask_len * sizeof(*bitmask));
348 value = _this->syms.udev_device_get_sysattr_value(pdev, attr);
349 if (!value) {
350 return;
351 }
352
353 SDL_strlcpy(text, value, sizeof(text));
354 i = 0;
355 while ((word = SDL_strrchr(text, ' ')) != NULL) {
356 v = SDL_strtoul(word + 1, NULL, 16);
357 if (i < bitmask_len) {
358 bitmask[i] = v;
359 }
360 ++i;
361 *word = '\0';
362 }
363 v = SDL_strtoul(text, NULL, 16);
364 if (i < bitmask_len) {
365 bitmask[i] = v;
366 }
367}
368
369static int guess_device_class(struct udev_device *dev)
370{
371 struct udev_device *pdev;
372 unsigned long bitmask_props[NBITS(INPUT_PROP_MAX)];
373 unsigned long bitmask_ev[NBITS(EV_MAX)];
374 unsigned long bitmask_abs[NBITS(ABS_MAX)];
375 unsigned long bitmask_key[NBITS(KEY_MAX)];
376 unsigned long bitmask_rel[NBITS(REL_MAX)];
377
378 /* walk up the parental chain until we find the real input device; the
379 * argument is very likely a subdevice of this, like eventN */
380 pdev = dev;
381 while (pdev && !_this->syms.udev_device_get_sysattr_value(pdev, "capabilities/ev")) {
382 pdev = _this->syms.udev_device_get_parent_with_subsystem_devtype(pdev, "input", NULL);
383 }
384 if (!pdev) {
385 return 0;
386 }
387
388 get_caps(dev, pdev, "properties", bitmask_props, SDL_arraysize(bitmask_props));
389 get_caps(dev, pdev, "capabilities/ev", bitmask_ev, SDL_arraysize(bitmask_ev));
390 get_caps(dev, pdev, "capabilities/abs", bitmask_abs, SDL_arraysize(bitmask_abs));
391 get_caps(dev, pdev, "capabilities/rel", bitmask_rel, SDL_arraysize(bitmask_rel));
392 get_caps(dev, pdev, "capabilities/key", bitmask_key, SDL_arraysize(bitmask_key));
393
394 return SDL_EVDEV_GuessDeviceClass(&bitmask_props[0],
395 &bitmask_ev[0],
396 &bitmask_abs[0],
397 &bitmask_key[0],
398 &bitmask_rel[0]);
399}
400
401static int device_class(struct udev_device *dev)
402{
403 const char *subsystem;
404 const char *val = NULL;
405 int devclass = 0;
406
407 subsystem = _this->syms.udev_device_get_subsystem(dev);
408 if (!subsystem) {
409 return 0;
410 }
411
412 if (SDL_strcmp(subsystem, "sound") == 0) {
413 devclass = SDL_UDEV_DEVICE_SOUND;
414 } else if (SDL_strcmp(subsystem, "video4linux") == 0) {
415 val = _this->syms.udev_device_get_property_value(dev, "ID_V4L_CAPABILITIES");
416 if (val && SDL_strcasestr(val, "capture")) {
417 devclass = SDL_UDEV_DEVICE_VIDEO_CAPTURE;
418 }
419 } else if (SDL_strcmp(subsystem, "input") == 0) {
420 // udev rules reference: http://cgit.freedesktop.org/systemd/systemd/tree/src/udev/udev-builtin-input_id.c
421
422 val = _this->syms.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK");
423 if (val && SDL_strcmp(val, "1") == 0) {
424 devclass |= SDL_UDEV_DEVICE_JOYSTICK;
425 }
426
427 val = _this->syms.udev_device_get_property_value(dev, "ID_INPUT_ACCELEROMETER");
428 if (val && SDL_strcmp(val, "1") == 0) {
429 devclass |= SDL_UDEV_DEVICE_ACCELEROMETER;
430 }
431
432 val = _this->syms.udev_device_get_property_value(dev, "ID_INPUT_MOUSE");
433 if (val && SDL_strcmp(val, "1") == 0) {
434 devclass |= SDL_UDEV_DEVICE_MOUSE;
435 }
436
437 val = _this->syms.udev_device_get_property_value(dev, "ID_INPUT_TOUCHSCREEN");
438 if (val && SDL_strcmp(val, "1") == 0) {
439 devclass |= SDL_UDEV_DEVICE_TOUCHSCREEN;
440 }
441
442 /* The undocumented rule is:
443 - All devices with keys get ID_INPUT_KEY
444 - From this subset, if they have ESC, numbers, and Q to D, it also gets ID_INPUT_KEYBOARD
445
446 Ref: http://cgit.freedesktop.org/systemd/systemd/tree/src/udev/udev-builtin-input_id.c#n183
447 */
448 val = _this->syms.udev_device_get_property_value(dev, "ID_INPUT_KEY");
449 if (val && SDL_strcmp(val, "1") == 0) {
450 devclass |= SDL_UDEV_DEVICE_HAS_KEYS;
451 }
452
453 val = _this->syms.udev_device_get_property_value(dev, "ID_INPUT_KEYBOARD");
454 if (val && SDL_strcmp(val, "1") == 0) {
455 devclass |= SDL_UDEV_DEVICE_KEYBOARD;
456 }
457
458 if (devclass == 0) {
459 // Fall back to old style input classes
460 val = _this->syms.udev_device_get_property_value(dev, "ID_CLASS");
461 if (val) {
462 if (SDL_strcmp(val, "joystick") == 0) {
463 devclass = SDL_UDEV_DEVICE_JOYSTICK;
464 } else if (SDL_strcmp(val, "mouse") == 0) {
465 devclass = SDL_UDEV_DEVICE_MOUSE;
466 } else if (SDL_strcmp(val, "kbd") == 0) {
467 devclass = SDL_UDEV_DEVICE_HAS_KEYS | SDL_UDEV_DEVICE_KEYBOARD;
468 }
469 } else {
470 // We could be linked with libudev on a system that doesn't have udev running
471 devclass = guess_device_class(dev);
472 }
473 }
474 }
475
476 return devclass;
477}
478
479static void device_event(SDL_UDEV_deviceevent type, struct udev_device *dev)
480{
481 int devclass = 0;
482 const char *path;
483 SDL_UDEV_CallbackList *item;
484
485 path = _this->syms.udev_device_get_devnode(dev);
486 if (!path) {
487 return;
488 }
489
490 if (type == SDL_UDEV_DEVICEADDED) {
491 devclass = device_class(dev);
492 if (!devclass) {
493 return;
494 }
495 } else {
496 // The device has been removed, the class isn't available
497 }
498
499 // Process callbacks
500 for (item = _this->first; item; item = item->next) {
501 item->callback(type, devclass, path);
502 }
503}
504
505void SDL_UDEV_Poll(void)
506{
507 struct udev_device *dev = NULL;
508 const char *action = NULL;
509
510 if (!_this) {
511 return;
512 }
513
514 while (SDL_UDEV_hotplug_update_available()) {
515 dev = _this->syms.udev_monitor_receive_device(_this->udev_mon);
516 if (!dev) {
517 break;
518 }
519 action = _this->syms.udev_device_get_action(dev);
520
521 if (action) {
522 if (SDL_strcmp(action, "add") == 0) {
523 device_event(SDL_UDEV_DEVICEADDED, dev);
524 } else if (SDL_strcmp(action, "remove") == 0) {
525 device_event(SDL_UDEV_DEVICEREMOVED, dev);
526 }
527 }
528
529 _this->syms.udev_device_unref(dev);
530 }
531}
532
533bool SDL_UDEV_AddCallback(SDL_UDEV_Callback cb)
534{
535 SDL_UDEV_CallbackList *item;
536 item = (SDL_UDEV_CallbackList *)SDL_calloc(1, sizeof(SDL_UDEV_CallbackList));
537 if (!item) {
538 return false;
539 }
540
541 item->callback = cb;
542
543 if (!_this->last) {
544 _this->first = _this->last = item;
545 } else {
546 _this->last->next = item;
547 _this->last = item;
548 }
549
550 return true;
551}
552
553void SDL_UDEV_DelCallback(SDL_UDEV_Callback cb)
554{
555 SDL_UDEV_CallbackList *item;
556 SDL_UDEV_CallbackList *prev = NULL;
557
558 if (!_this) {
559 return;
560 }
561
562 for (item = _this->first; item; item = item->next) {
563 // found it, remove it.
564 if (item->callback == cb) {
565 if (prev) {
566 prev->next = item->next;
567 } else {
568 SDL_assert(_this->first == item);
569 _this->first = item->next;
570 }
571 if (item == _this->last) {
572 _this->last = prev;
573 }
574 SDL_free(item);
575 return;
576 }
577 prev = item;
578 }
579}
580
581const SDL_UDEV_Symbols *SDL_UDEV_GetUdevSyms(void)
582{
583 if (!SDL_UDEV_Init()) {
584 SDL_SetError("Could not initialize UDEV");
585 return NULL;
586 }
587
588 return &_this->syms;
589}
590
591void SDL_UDEV_ReleaseUdevSyms(void)
592{
593 SDL_UDEV_Quit();
594}
595
596#endif // SDL_USE_LIBUDEV
597