Repository navigation
Incorrect device node found when creating more than one UInput device #205
Description
Sample code:
import evdev
import evdev.ecodes as e
x = evdev.UInput({e.EV_KEY: [e.KEY_A]})
y = evdev.UInput({e.EV_REL: [e.REL_X]})
print("Device node of the first device: ", x.device)
print("Device node of the second device: ", y.device)
print("Capabilities of the first device: ", x.capabilities())
print("Capabilities of the second device: ", y.capabilities())Output on my system:
Device node of the first device: device /dev/input/event20, name "py-evdev-uinput", phys "py-evdev-uinput"
Device node of the second device: device /dev/input/event20, name "py-evdev-uinput", phys "py-evdev-uinput"
Capabilities of the first device: {0: [0, 1, 21], 1: [30]}
Capabilities of the second device: {0: [0, 1, 21], 1: [30]}
The problem:
Even though I have created two different UInput devices, but when I try to query their device node using UInput.device, both of them return the same device node. This has consequences beyond just the device node: as you can see, both of them report having the same capabilities, even though they were clearly created with different capabilities.
Evtest confirms that python-evdev did properly create two virtual devices, and it is possible to write events to these devices. The only problem seems to be that python-evdev reports incorrect information back to the programmer.
The cause:
I've looked through the source code, and it seems that the device node is discovered through the following code in uinput.py:
def _find_device(self):
#:bug: the device node might not be immediately available
time.sleep(0.1)
for path in util.list_devices('/dev/input/'):
d = device.InputDevice(path)
if d.name == self.name:
return dAs you can see for yourself, this code looks quite fragile. It does not inherently look up the device node of the created input device, but instead searches for the first input device that has the same name. Since both created input devices have the name "py-evdev-uinput", this code may return either device.
I suppose that this code will probably return the correct node if list_devices returns the device nodes from newest device to oldest device. The function list_devices is implemented using glob.glob, and I do not really see any guarantee that glob returns files in that order, so I suppose that whether your system suffers from this bug depends on the order in which your operating system happens to list the device nodes.
Further information:
My operating system is Arch Linux. Python-evdev has been installed on my system by the OS package manager.
$ python -V
Python 3.11.6
$ pacman -Q python-evdev
python-evdev 1.6.1-2
$ uname -r
6.6.8-zen1-1-zen
Activity
sezanzeb commentedon Jan 4, 2024
On ubuntu 23.04 and the latest python-evdev:
I have done some experiments, and the modification date of the devnodes is all over the place.
After the first uinput has been created:
After the second uinput has been created:
However, the name of the devnode seems to be in order. Sorting by name in reverse might just do the trick:
KarsMulder commentedon Jan 4, 2024
Thanks for the quick response!
I suppose that that will work well enough in most cases (it at least works well enough for my use case.) I am personally not a fan of a solution with race conditions like this because it could mysteriously break if an user has written two different python-evdev script and configured systemd to start both of them at boot; in that case causing a race condition within a window as large as 0.1s seems to be pretty much guaranteed.
I would want to say "just use
libevdev_uinput_get_devnodeto do it properly", but it seems that python-evdev does not actually rely on libevdev and instead implements the ioctls itself. The functionlibevdev_uinput_get_devnodeseems to do the following:On Linux:
Libevdev uses the
UI_GET_SYSNAMEioctl to get the system name of the device, e.g.input23.It then looks at the filenames in the directory
/sys/devices/virtual/input/input23and expects it to contain one directory matchingevent*, e.g./sys/devices/virtual/input/input23/event20.Then it assumes that
/dev/input/event20is the corresponding device node.On FreeBSD:
Libevdev uses the
UI_GET_SYSNAMEioctl to get the system name of the device, e.g.event20, and then assumes that/dev/input/event20is the corresponding device node....
The above relies on UI_GET_SYSNAME being available, which seems to be added to the Linux kernel in 2014. Libevdev contains some more code relying on ctime to work with old kernels. As the libevdev documentation itself says:
On libevdev_uinput_get_devnode:
On libevdev_uinput_get_syspath:
This solution is not quite ideal either since it does rely on quite some assumptions on the user's system and the layout of
/sys, but assumingUI_GET_SYSNAMEis available, the code is at least immune to race conditions caused by different scripts creating devices simultaneously.If we were to add a binding to the
UI_GET_SYSNAMEioctl in theevdev/uinput.cfile, then the rest of the logic can be implemented in pure Python code. If you want, I can work on a pull request to port libevdev's logic to python-evdev?sezanzeb commentedon Jan 5, 2024
Sure, please feel free to do so. I'll review and test it as good as I can. However, bear in mind that I'm not a c developer, so I won't be able to comment on anything related to memory management, performance and code quality and the likes.
KarsMulder commentedon Jan 6, 2024
I've gotten around to actually testing the proposed code with a little print for debugging:
And the devices that got printed were:
As you can see, using the default alphabetic sort, we're not getting the correct ordering, because
/dev/input/event9would end up getting checked before/dev/event/10, which leads to those wonderful bugs that have only a one-in-ten chance of occurring.I added a commit with another sorting algorithm to my pull request.
KarsMulder commentedon Jan 10, 2024
This issue has been resolved by pull request #206.