swhkd
swhkd copied to clipboard
`swhkd` does not process hotkeys of newly connected devices
Version Information:
- Linux arch-dktp-alexis 5.19.6-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 31 Aug 2022 22:09:40 +0000 x86_64 GNU/Linux
- swhkd 1.2.1 (installed from the
swhkd-gitAUR package)
Describe the bug:
swhkd does not register hotkeys from a newly connected keyboard (or disconnected then reconnected).
Expected behavior:
swhkd should be able to register hotkeys on a newly connected keyboard.
Actual behavior:
swhkd does not grab newly connected devices and thus cannot handle their keypresses events.
To Reproduce:
Start swhkd. Connect a keyboard (or disconnect yours before reconnecting it). Hotkeys will not work.
Additional information:
This can probably be solved using udev, with either the udev crate or tokio-udev crate.
My attempts at implementing this (the same content is also in the [wip] commits of each branch):
- Using the
tokio-udevcrate (udev_asyncbranch):This does not work as-is. Unfortunately, sometimes udev events get 'stuck' and only processed after another batch of events comes in. In practice, this translates to e.g. a batch of 'Add' events (after connecting a device) only passed to the application loop after a batch of 'Remove' events (after disconnecting the same device for instance).
- Using the
udevcrate iterators wrapped in a tokio stream (udev_syncbranch):This does not work as-is, as the
MonitorSocketIteratormay returnNonewhen no udev events are available. tokio stops listening after the firstNonereceived from the stream, which means that events are not listened to at all. - Using the
udevcrate with a busy loop (udev_sync_busybranch):At the cost of an always busy thread, this does receive all udev events in a timely manner.
The busy loop is the only one which is working, but also the worst performance-wise. See this one more as a proof of concept.
MWE for each implementation
Each exemple prints events from the 'input' subsystem as they are received. Each log is prefixed by a basic timestamp to highlight the difference between the actual physical event and the time it is passed to the application.
Busy loop
use std::error::Error;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::select;
use tokio_stream::StreamExt;
use udev::MonitorBuilder;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let mut udev = tokio_stream::iter(MonitorBuilder::new()?.match_subsystem("input")?.listen()?);
loop {
select! {
udev_item = udev.next() => {
if udev_item.is_none() {
continue;
}
let event = udev_item.unwrap();
println!("[{:?}] '{:?}' event - devnode: {:?}",
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
event.event_type(),
event.devnode());
}
}
}
}
On my computer, when disconnecting and reconnecting my keyboard, this gives:
[1663146489] 'Remove' event - devnode: Some("/dev/input/event3") [1663146489] 'Remove' event - devnode: None [1663146489] 'Remove' event - devnode: Some("/dev/input/event5") [1663146489] 'Remove' event - devnode: None [1663146489] 'Remove' event - devnode: Some("/dev/input/event6") [1663146489] 'Remove' event - devnode: None [1663146489] 'Remove' event - devnode: Some("/dev/input/event7") [1663146489] 'Remove' event - devnode: None [1663146489] 'Remove' event - devnode: Some("/dev/input/event8") [1663146489] 'Remove' event - devnode: None [1663146491] 'Add' event - devnode: None [1663146491] 'Add' event - devnode: None [1663146491] 'Add' event - devnode: None [1663146491] 'Add' event - devnode: None [1663146491] 'Add' event - devnode: None [1663146491] 'Add' event - devnode: Some("/dev/input/event5") [1663146491] 'Add' event - devnode: Some("/dev/input/event7") [1663146491] 'Add' event - devnode: Some("/dev/input/event6") [1663146491] 'Add' event - devnode: Some("/dev/input/event8") [1663146491] 'Add' event - devnode: Some("/dev/input/event3")
Sync loop
Replace the inner loop with:
select! {
Some(event) = udev.next() => {
println!("[{:?}] '{:?}' event - devnode: {:?}",
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
event.event_type(),
event.devnode());
}
else => {}
}
This does work in this example, but events are ignored when trying the same thing in swhkd. I think this is because the stream is ignored by tokio on the first None it returns.
Async
use std::error::Error;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::select;
use tokio_stream::StreamExt;
use tokio_udev::{AsyncMonitorSocket, MonitorBuilder};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let mut udev = AsyncMonitorSocket::new(MonitorBuilder::new()?.match_subsystem("input")?.listen()?)?;
loop {
select! {
Some(Ok(event)) = udev.next() => {
println!("[{:?}] '{:?}' event - devnode: {:?}",
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
event.event_type(),
event.devnode());
}
}
}
}
For two disconnections/reconnections, this gives me (late events in bold):
[1663146903] 'Remove' event - devnode: Some("/dev/input/event3") [1663146903] 'Remove' event - devnode: None [1663146903] 'Remove' event - devnode: Some("/dev/input/event5") [1663146903] 'Remove' event - devnode: None [1663146903] 'Remove' event - devnode: Some("/dev/input/event6") [1663146903] 'Remove' event - devnode: None [1663146903] 'Remove' event - devnode: Some("/dev/input/event7") [1663146903] 'Remove' event - devnode: Some("/dev/input/event8") [1663146903] 'Remove' event - devnode: None [1663146919] 'Remove' event - devnode: None [1663146919] 'Add' event - devnode: None [1663146919] 'Add' event - devnode: None [1663146919] 'Add' event - devnode: None [1663146919] 'Add' event - devnode: None [1663146919] 'Add' event - devnode: None [1663146919] 'Add' event - devnode: Some("/dev/input/event6") [1663146919] 'Add' event - devnode: Some("/dev/input/event8") [1663146919] 'Add' event - devnode: Some("/dev/input/event5") [1663146919] 'Add' event - devnode: Some("/dev/input/event7") [1663146925] 'Add' event - devnode: Some("/dev/input/event3") [1663146925] 'Remove' event - devnode: Some("/dev/input/event3") [1663146925] 'Remove' event - devnode: None [1663146925] 'Remove' event - devnode: Some("/dev/input/event5") [1663146925] 'Remove' event - devnode: None [1663146925] 'Remove' event - devnode: Some("/dev/input/event6") [1663146925] 'Remove' event - devnode: None [1663146925] 'Remove' event - devnode: Some("/dev/input/event7") [1663146925] 'Remove' event - devnode: None [1663146925] 'Remove' event - devnode: Some("/dev/input/event8") [1663146932] 'Remove' event - devnode: None [1663146932] 'Add' event - devnode: None [1663146932] 'Add' event - devnode: None [1663146932] 'Add' event - devnode: None [1663146932] 'Add' event - devnode: None [1663146932] 'Add' event - devnode: None [1663146932] 'Add' event - devnode: Some("/dev/input/event6") [1663146932] 'Add' event - devnode: Some("/dev/input/event8") [1663146932] 'Add' event - devnode: Some("/dev/input/event5")
Please note that here, the input which is the actual keyboard is /dev/input/event3 but it is passed to the application only after it has been removed again.
I do not understand why this happens. The most I was able to understand was that the stream sometimes got 'stuck' on Poll::Pending as if no new events were available (but there actually was).
Running udevadm monitor -s input at the same time shows events in real time with no lag, as the busy loop does. I was not able to track this issue further for now.
I looked into solving my issue with udev, but if anyone has any other idea, I can also take a shot at it !
Wow! Impressive bug report and you have my respect for putting in the time to not only investigate the issue but also to propose possible changes ❤️. The implementations look interesting, I'm not too familiar with udev but I just had one question, can udev detect the type of device that we're dealing with? this can help another bug where mice are picked up as keyboards sometimes.
Apart from that, this looks good, I'll review it in depth once I'm done with relocating!
I did some digging and found out that we don't need to create a new MonitorSocket from MonitorBuilder::new()?....listen(). We can create this builder once globally and then just poll the socket. Although this too is inefficient, it's...not as bad 🤣.
can udev detect the type of device that we're dealing with? this can help another bug where mice are picked up as keyboards sometimes.
Maybe?
Take a look at https://wiki.ubuntu.com/X/InputConfiguration#Device_classification: udev classifies devices when detected. With some basic testing on my side, it seems that my keyboard is indeed detected as ID_INPUT_KEYBOARD and my mouse as ID_INPUT_MOUSE... and also ID_INPUT_KEYOARD. My mouse is connected through a Logitech receiver which works for both their mice and keyboards, so it advertises for both (even though I do not have one of their keyboards). This may be better than the current keyboard detection, but it may still yield false positives.
A basic implementation could be:
// or tokio_udev
let mut udev_scan = udev::Enumerator::new()?;
udev_scan.match_subsystem("input")?;
udev_scan.match_property("ID_INPUT_KEYBOARD", "1")?;
let keyboard_devices = udev_scan.scan_devices()?
.map(|dev| dev.devnode().map(|path| path.to_owned()))
.flatten()
.map(evdev::Device::open)
.flatten()
.collect::<Vec<_>>();
This works with some basic testing. I could implement a PR for that if you'd like!
To be honest, I think it would be better to fix #131 rather than detecting keyboards: if swhkd could properly re-emit events from all kinds of devices, there would be no need to only grab keyboards, right?
I did some digging and found out that we don't need to create a new
MonitorSocketfromMonitorBuilder::new()?....listen(). We can create this builder once globally and then just poll the socket. Although this too is inefficient, it's...not as bad :rofl:.
Is it that bad? Isn't the socket only created once anyway? What does it change to create first the builder?
I did some digging and found out that we don't need to create a new
MonitorSocketfromMonitorBuilder::new()?....listen(). We can create this builder once globally and then just poll the socket. Although this too is inefficient, it's...not as bad :rofl:.Is it that bad? Isn't the socket only created once anyway? What does it change to create first the builder?
It's probably not too different but why perform extra computation when we can just poll the socket descriptor?
To be honest, I think it would be better to fix #131 rather than detecting keyboards: if
swhkdcould properly re-emit events from all kinds of devices, there would be no need to only grab keyboards, right?
Okay! it's cool that udev can detect these things for me, intruiging. However I think you're correct, we can drop this task from udev entirely and just implement good mouse support too. I have attempted this in an open pr but failed eventually, would you like to try your hand at it? I'll help as much as I can.
Also I've been meaning to ask, would you like to join us as a maintainer? I've noticed that you have great debugging skills along with good problem solving skills.
I have time this weekend to work on the udev problem! I'll go ahead and start on a patch asap. PS: Cleared up some unnecessary messages.
I have reported this issue to tokio-udev upstream: https://github.com/jeandudey/tokio-udev/issues/17 Hopefully they can help us!
https://app.bountysource.com/issues/118215631-swhkd-does-not-process-hotkeys-of-newly-connected-devices
If anyone is interested!
The latest updates in the tokio_udev repo make it possible to detect added/removed devices.
This is implemented in the udev_async branch.
I will be waiting for a release of tokio_udev before creating a PR for it.
https://github.com/jeandudey/tokio-udev/issues/19
@ajanon I tested the udev_async branch and it seems that sometimes, the code seems to panic. I am yet to find the triggering cause. I'll paste the logs below:
[2023-05-30T03:18:22Z INFO swhkd] Device 'SINO WEALTH Gaming KB ' removed
[2023-05-30T03:18:22Z INFO swhkd] Device 'SINO WEALTH Gaming KB Keyboard' removed
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 2', swhkd/src/daemon.rs:331:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Here are the steps I found to reliably reproduce the issue. I have an external keyboard connected to my laptop. Upon disconnecting my external keyboard and attempting to press just a modifier key ( in this case, Super ) It causes swhkd to crash. It seems to be crashing on the keyboard_stream_map match.
When running from the main branch swhkd doesn't give any error but it doesn't seem to accept keyboard events any longer. When running from the udev_async branch I get the same error as @Shinyzenith .
[2023-06-03T15:31:04Z INFO swhkd] Device 'Gaming keyboard' removed
[2023-06-03T15:31:04Z INFO swhkd] Device 'Gaming keyboard' removed
[2023-06-03T15:31:04Z INFO swhkd] Device 'Gaming keyboard' removed
thread 'main' panicked at 'removal index (is 3) should be < len (is 1)', swhkd/src/daemon.rs:318:49
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted
Is this still an issue?
I wanted to look into it because of the bounty but I can't reproduce the issue with the udev_async head. I can plug and unplug my USB keyboard freely and swhkd seems to cope just fine.
(Incidentally I am doing this on a setup where no commands are actually run, on any hotkey swhkd prints "failed to send command through ipc". swhkd can't find the socket created by swhkd for reasons unclear to me. But that can't be the reason for the error not happening to me, can it?)
I have detailed the steps to a crash issue two comments above. Can you reproduce that?
I have detailed the steps to a crash issue two comments above. Can you reproduce that?
I can connect and disconnect the USB keyboard, and pressing super in both states, on both keyboards (obvs on the external only when connected), makes swhkd log the key. Using any combinations involving super also are logged fine (though the commands not executed, as I mentioned above).
The only combination in my config is a Super + l. I don't think the configured combinations should be relevant to the location of the crash either, however.
Here are the steps I found to reliably reproduce the issue. I have an external keyboard connected to my laptop. Upon disconnecting my external keyboard and attempting to press just a modifier key ( in this case, Super ) It causes swhkd to crash. It seems to be crashing on the keyboard_stream_map match.
Am I missing something?
Fixed the socket issue with commands, it was just polkit being weird like always. still can't reproduce the crash.
Wow, that's some main(). Looking at it, I might have some theories, even if I can't reproduce it personally. Is it alright if I make some educated-guess changes and ask any of you (who suffer from the crash) to test?
Still don't face the issue, but I came up with some theories and made some fixes to obvious problems: https://github.com/ConcurrentCrab/swhkd/tree/udev_async https://github.com/waycrate/swhkd/pull/216
@Shinyzenith and others: can you test and see if these fixes work for you?
Still don't face the issue, but I came up with some theories and made some fixes to obvious problems: https://github.com/ConcurrentCrab/swhkd/tree/udev_async #216
Nice, seems to be working now. Great job!
[2023-07-18T20:11:23Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event2' removed
[2023-07-18T20:11:23Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event3' removed
[2023-07-18T20:11:23Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event4' removed
[2023-07-18T20:11:23Z INFO swhkd] Device 'Logitech Gaming Mouse G502 Keyboard' at '/dev/input/event1' removed
[2023-07-18T20:11:31Z ERROR swhkd] Could not open evdev device at /dev/input/mouse0: Inappropriate ioctl for device (os error 25)
[2023-07-18T20:11:31Z DEBUG swhkd] Keyboard: Logitech Gaming Mouse G502 Keyboard
[2023-07-18T20:11:31Z INFO swhkd] Device 'Logitech Gaming Mouse G502 Keyboard' at '/dev/input/event1' added.
[2023-07-18T20:11:31Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-18T20:11:31Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event2' added.
[2023-07-18T20:11:31Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-18T20:11:31Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event3' added.
[2023-07-18T20:11:31Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-18T20:11:31Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event4' added.
[2023-07-18T20:11:31Z TRACE swhkd] Other: Logitech Gaming Mouse G502
Fixed the socket issue with commands, it was just polkit being weird like always. still can't reproduce the crash.
Wow, that's some
main(). Looking at it, I might have some theories, even if I can't reproduce it personally. Is it alright if I make some educated-guess changes and ask any of you (who suffer from the crash) to test?
Sorry for the late reply. I'm more than happy to do some more testing. Yeah the main() needs cleanup...a lot of it.
I'll review the PR and get that going.
I have merged the pr sent by @ConcurrentCrab. Just to be sure @CluelessTechnologist would you mind testing the latest HEAD of udev branch once more? If all is good to go, I'll merge.
I have merged the pr sent by @ConcurrentCrab. Just to be sure @CluelessTechnologist would you mind testing the latest HEAD of udev branch once more? If all is good to go, I'll merge.
Sure. Just tested the latest commit from udev branch. Happy to report that it's still working:
[2023-07-19T22:49:04Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event2' removed
[2023-07-19T22:49:04Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event3' removed
[2023-07-19T22:49:04Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event4' removed
[2023-07-19T22:49:04Z INFO swhkd] Device 'Logitech Gaming Mouse G502 Keyboard' at '/dev/input/event1' removed
[2023-07-19T22:49:11Z ERROR swhkd] Could not open evdev device at /dev/input/mouse0: Inappropriate ioctl for device (os error 25)
[2023-07-19T22:49:11Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-19T22:49:11Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event2' added.
[2023-07-19T22:49:11Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-19T22:49:11Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event4' added.
[2023-07-19T22:49:11Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-19T22:49:11Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event3' added.
[2023-07-19T22:49:11Z DEBUG swhkd] Keyboard: Logitech Gaming Mouse G502 Keyboard
[2023-07-19T22:49:11Z INFO swhkd] Device 'Logitech Gaming Mouse G502 Keyboard' at '/dev/input/event1' added.
[2023-07-19T22:49:11Z TRACE swhkd] Other: Logitech Gaming Mouse G502
@CluelessTechnologist I made some minor changes and force pushed. Would you mind testing the branch head one last time? My apologies. After that it'll be ready for merge.
@CluelessTechnologist I made some minor changes and force pushed. Would you mind testing the branch head one last time? My apologies. After that it'll be ready for merge.
Okay I pulled the latest commit and recompiled and reinstalled. Still seems to be working:
[2023-07-21T13:44:11Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event2' removed
[2023-07-21T13:44:11Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event3' removed
[2023-07-21T13:44:11Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event4' removed
[2023-07-21T13:44:11Z INFO swhkd] Device 'Logitech Gaming Mouse G502 Keyboard' at '/dev/input/event1' removed
[2023-07-21T13:44:18Z ERROR swhkd] Could not open evdev device at /dev/input/mouse0: Inappropriate ioctl for device (os error 25)
[2023-07-21T13:44:18Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-21T13:44:18Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event2' added.
[2023-07-21T13:44:18Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-21T13:44:18Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event4' added.
[2023-07-21T13:44:18Z DEBUG swhkd] Keyboard: Logitech Gaming Mouse G502 Keyboard
[2023-07-21T13:44:18Z INFO swhkd] Device 'Logitech Gaming Mouse G502 Keyboard' at '/dev/input/event1' added.
[2023-07-21T13:44:18Z DEBUG swhkd] Keyboard: Gaming keyboard
[2023-07-21T13:44:18Z INFO swhkd] Device 'Gaming keyboard' at '/dev/input/event3' added.
[2023-07-21T13:44:18Z TRACE swhkd] Other: Logitech Gaming Mouse G502
Nice, seems to be working now. Great job!
Awesome! Happy to be of help to y'all, and thanks for the bounty!
@Shinyzenith I can't collect the bounty on bountysource.com until this issue is marked as closed, so I'd really appreciate it if you could close it and test miscelleneous changes afterwards :P