Skip to content

Commit

Permalink
fix(win-llhook): before blocking, sync Win VKs and Kanata (#1324)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtroo authored Nov 3, 2024
1 parent 4ab74b8 commit 8b84101
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 8 deletions.
4 changes: 2 additions & 2 deletions keyberon/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,14 @@ impl<'a, T> Clone for State<'a, T> {
}
}
impl<'a, T: 'a> State<'a, T> {
fn keycode(&self) -> Option<KeyCode> {
pub fn keycode(&self) -> Option<KeyCode> {
match self {
NormalKey { keycode, .. } => Some(*keycode),
FakeKey { keycode } => Some(*keycode),
_ => None,
}
}
fn coord(&self) -> Option<KCoord> {
pub fn coord(&self) -> Option<KCoord> {
match self {
NormalKey { coord, .. }
| LayerModifier { coord, .. }
Expand Down
6 changes: 6 additions & 0 deletions parser/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,12 @@ impl From<OsCode> for u32 {
}
}

impl From<OsCode> for i32 {
fn from(item: OsCode) -> Self {
item.as_u16() as i32
}
}

impl From<OsCode> for u16 {
fn from(item: OsCode) -> Self {
item.as_u16()
Expand Down
22 changes: 18 additions & 4 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ pub struct Kanata {
/// Handle to the keyberon library layout.
pub layout: cfg::KanataLayout,
/// Reusable vec (to save on allocations) that stores the currently active output keys.
/// This can be cleared and reused in various procedures as buffer space.
pub cur_keys: Vec<KeyCode>,
/// Reusable vec (to save on allocations) that stores the active output keys from the previous
/// tick.
/// tick. This must only be updated once per tick and must not be modified outside of the one
/// procedure that updates it.
pub prev_keys: Vec<KeyCode>,
/// Used for printing layer info to the info log when changing layers.
pub layer_info: Vec<LayerInfo>,
Expand Down Expand Up @@ -1788,6 +1790,13 @@ impl Kanata {
k.can_block_update_idle_waiting(ms_elapsed)
};
if can_block {
#[cfg(all(
target_os = "windows",
not(feature = "interception_driver"),
not(feature = "simulated_input"),
))]
kanata.lock().win_synchronize_keystates();

log::trace!("blocking on channel");
match rx.recv() {
Ok(kev) => {
Expand All @@ -1804,7 +1813,7 @@ impl Kanata {
// If kanata has been inactive for long enough, clear all states.
// This won't trigger if there are macros running, or if a key is
// held down for a long time and is sending OS repeats. The reason
// for this code is in case like Win+L which locks the Windows
// for this code is in cases like Win+L which locks the Windows
// desktop. When this happens, the Win key and L key will be stuck
// as pressed in the kanata state because LLHOOK kanata cannot read
// keys in the lock screen or administrator applications. So this
Expand All @@ -1816,7 +1825,7 @@ impl Kanata {
// a fake key pressed for a long period of time, so make sure those
// are not cleared.
if (now - last_input_time)
> time::Duration::from_secs(LLHOOK_IDLE_TIME_CLEAR_INPUTS)
> time::Duration::from_secs(LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS)
{
log::debug!(
"clearing keyberon normal key states due to inactivity"
Expand Down Expand Up @@ -1952,7 +1961,7 @@ impl Kanata {
// a fake key pressed for a long period of time, so make sure those
// are not cleared.
if (instant::Instant::now() - (last_input_time))
> time::Duration::from_secs(LLHOOK_IDLE_TIME_CLEAR_INPUTS)
> time::Duration::from_secs(LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS)
&& !idle_clear_happened
{
idle_clear_happened = true;
Expand All @@ -1979,6 +1988,11 @@ impl Kanata {
});
}

/// Returns `true` if kanata's processing thread loop can block on the channel instead of doing
/// a non-blocking channel read and then sleeping for ~1ms.
///
/// In addition to doing the logic for the above, this mutates the `waiting_for_idle` state
/// used by the `on-idle` action for virtual keys.
pub fn can_block_update_idle_waiting(&mut self, ms_elapsed: u16) -> bool {
let k = self;
let is_idle = k.is_idle();
Expand Down
128 changes: 128 additions & 0 deletions src/kanata/windows/llhook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,134 @@ impl Kanata {
native_windows_gui::dispatch_thread_events();
Ok(())
}

/// On Windows with LLHOOK/SendInput APIs,
/// Kanata does not have as much control
/// over the full system's keystates as one would want;
/// unlike in Linux or with the Interception driver.
/// Sometimes Kanata can miss events; e.g. a release is
/// missed and a keystate remains pressed within Kanata (1),
/// or a press is missed in Kanata but the release is caught,
/// and thus the keystate remains pressed within the Windows system
/// because Kanata consumed the release and didn't know what to do about it (2).
///
/// For (1), `release_normalkey_states` theoretically fixes the issue
/// after 60s of Kanata being idle,
/// but that is a long time and doesn't seem to work consistently.
/// Unfortunately this does not seem to be easily fixable in all cases.
/// For example, a press consumed by Kanata could result in
/// **only** a `(layer-while-held ...)` action as the output;
/// if the corresponding release were missed,
/// Kanata has no information available from the larger Windows system
/// to confirm that the physical key is actually released
/// but that the process didn't see the event.
/// E.g. there is the `GetAsyncKeyState` API
/// and this will be useful when the missed release has a key output,
/// but not with the layer example.
/// There does not appear to be any "raw input" mechanism
/// to see the snapshot of the current state of physical keyboard keys.
///
/// For (2), consider that this might be fixed purely within Kanata's
/// event handling and processing, by checking Kanata's active action states,
/// and if there are no active states corresponding to a released event,
/// to send a release of the original input.
/// This would result in extra release events though;
/// for example if the `A` key action is `(macro a)`,
/// the above logic will result in a second SendInput release event of `A`.
/// Instead, this function checks against the outside Windows state.
///
/// The solution makes use of the following states:
/// - `MAPPED_KEYS` (MK)
/// - `GetAsyncKeyState` WinAPI (GKS)
/// - `PRESSED_KEYS` (PK)
/// - `self.prev_keys` (SPV)
///
/// If a discrepancy is detected,
/// this procedure releases Windows keys via SendInput
/// and/or clears internal Kanata states.
///
/// The checks are:
/// 1. For all of SPV, check that it is pressed in GKS.
/// If a key is not pressed, find the coordinate of this state.
/// Clear in PK and clear all states with the same coordinate as key output.
/// 2. For all keys in MK and active in GKS, check it is in SPV.
/// If not in SPV, call SendInput to release in Windows.
#[cfg(not(feature = "simulated_input"))]
pub(crate) fn win_synchronize_keystates(&mut self) {
use kanata_keyberon::layout::*;
use winapi::um::winuser::*;

log::debug!("synchronizing win keystates");
for pvk in self.prev_keys.iter() {
// Check 1 : each pvk is expected to be pressed.
let osc: OsCode = pvk.into();
let vk = i32::from(osc);
let vk_state = unsafe { GetAsyncKeyState(vk) } as u32;
let is_pressed_in_windows = vk_state >= 0b1000000;
if is_pressed_in_windows {
continue;
}

log::error!("Unexpected keycode is pressed in kanata but not in Windows. Clearing kanata states: {pvk}");
// Need to clear internal state about this key.
// find coordinate(s) in keyberon associated with pvk
let mut coords_to_clear = Vec::<KCoord>::new();
let layout = self.layout.bm();
layout.states.retain(|s| {
let retain = match s.keycode() {
Some(k) => k != *pvk,
_ => true,
};
if !retain {
if let Some(coord) = s.coord() {
coords_to_clear.push(coord);
}
}
retain
});

// Clear other states other than keycode associated with a keycode that needs to be
// cleaned up.
layout.states.retain(|s| match s.coord() {
Some(c) => !coords_to_clear.contains(&c),
None => false,
});

// Clear PRESSED_KEYS for coordinates associated with real and not virtual keys
let mut pressed_keys = PRESSED_KEYS.lock();
for osc in coords_to_clear.iter().copied().filter_map(|c| match c {
(FAKE_KEY_ROW, _) => None,
(_, kc) => Some(OsCode::from(kc)),
}) {
pressed_keys.remove(&osc);
}
drop(pressed_keys);
}

let mapped_keys = MAPPED_KEYS.lock();
for mapped_osc in mapped_keys.iter().copied() {
// Check 2: each active win vk mapped in Kanata should have a value in pvk
let vk = i32::from(mapped_osc);
if vk >= 256 {
continue;
}
let vk_state = unsafe { GetAsyncKeyState(vk) } as u32;
let is_pressed_in_windows = vk_state >= 0b1000000;
if !is_pressed_in_windows {
continue;
}
let vk = vk as u16;
let Some(osc) = OsCode::from_u16(vk) else {
continue;
};
if self.prev_keys.contains(&osc.into()) {
continue;
}
log::error!("Unexpected keycode is pressed in Windows but not Kanata. Releasing in Windows: {osc}");
let _ = release_key(&mut self.kbd_out, osc);
}
drop(mapped_keys);
}
}

fn try_send_panic(tx: &Sender<KeyEvent>, kev: KeyEvent) {
Expand Down
2 changes: 1 addition & 1 deletion src/oskbd/windows/exthook_os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use kanata_keyberon::key_code::KeyCode;

use kanata_parser::keys::*;

pub const LLHOOK_IDLE_TIME_CLEAR_INPUTS: u64 = 60;
pub const LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS: u64 = 60;

type HookFn = dyn FnMut(InputEvent) -> bool + Send + Sync + 'static;

Expand Down
2 changes: 1 addition & 1 deletion src/oskbd/windows/llhook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use kanata_keyberon::key_code::KeyCode;
use kanata_parser::custom_action::*;
use kanata_parser::keys::*;

pub const LLHOOK_IDLE_TIME_CLEAR_INPUTS: u64 = 60;
pub const LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS: u64 = 60;

type HookFn = dyn FnMut(InputEvent) -> bool;

Expand Down

0 comments on commit 8b84101

Please sign in to comment.