-
Notifications
You must be signed in to change notification settings - Fork 129
test(windows): 增加低层热键钩子回归 #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| param( | ||
| [string]$ExePath = "", | ||
| [int]$TimeoutSeconds = 20, | ||
| [int]$VirtualKey = 0xA3 | ||
| ) | ||
|
|
||
| $ErrorActionPreference = "Stop" | ||
|
|
||
| if ([string]::IsNullOrWhiteSpace($ExePath)) { | ||
| $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path | ||
| $ExePath = Join-Path $appRoot "src-tauri\target\x86_64-pc-windows-gnu\release\openless.exe" | ||
| } | ||
|
|
||
| if (-not $env:SystemDrive) { | ||
| $env:SystemDrive = "C:" | ||
| } | ||
| if (-not $env:ProgramData) { | ||
| $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" | ||
| } | ||
|
|
||
| if (-not (Test-Path $ExePath)) { | ||
| throw "OpenLess executable not found: $ExePath" | ||
| } | ||
|
|
||
| Add-Type @" | ||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| public static class OpenLessInput { | ||
| [DllImport("user32.dll")] | ||
| public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); | ||
|
|
||
| [DllImport("user32.dll")] | ||
| public static extern bool SetForegroundWindow(IntPtr hWnd); | ||
|
|
||
| [DllImport("user32.dll")] | ||
| public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, UIntPtr dwExtraInfo); | ||
|
|
||
| public const int KEYEVENTF_EXTENDEDKEY = 0x0001; | ||
| public const int KEYEVENTF_KEYUP = 0x0002; | ||
| } | ||
| "@ | ||
|
|
||
| function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) { | ||
| $deadline = (Get-Date).AddSeconds($TimeoutSeconds) | ||
| while ((Get-Date) -lt $deadline) { | ||
| if (Test-Path $Path) { | ||
| $text = Get-Content -Raw $Path | ||
| if ($text -match $Pattern) { | ||
| return $true | ||
| } | ||
| } | ||
| Start-Sleep -Milliseconds 250 | ||
| } | ||
| return $false | ||
| } | ||
|
|
||
| function Send-KeyEdge($Vk, $KeyUp) { | ||
| $flags = [OpenLessInput]::KEYEVENTF_EXTENDEDKEY | ||
| if ($KeyUp) { | ||
| $flags = $flags -bor [OpenLessInput]::KEYEVENTF_KEYUP | ||
| } | ||
| [OpenLessInput]::keybd_event([byte]$Vk, 0x1D, $flags, [UIntPtr]::Zero) | ||
| } | ||
|
|
||
| function Focus-Window($Process) { | ||
| if ($null -eq $Process -or $Process.MainWindowHandle -eq 0) { | ||
| return $false | ||
| } | ||
| [OpenLessInput]::ShowWindow($Process.MainWindowHandle, 9) | Out-Null | ||
| [OpenLessInput]::SetForegroundWindow($Process.MainWindowHandle) | Out-Null | ||
| Start-Sleep -Milliseconds 500 | ||
| return $true | ||
| } | ||
|
|
||
| function Wait-ProcessWindow($ProcessName, $After, $TimeoutSeconds) { | ||
| $deadline = (Get-Date).AddSeconds($TimeoutSeconds) | ||
| while ((Get-Date) -lt $deadline) { | ||
| $candidates = Get-Process $ProcessName -ErrorAction SilentlyContinue | | ||
| Where-Object { $_.StartTime -ge $After -and $_.MainWindowHandle -ne 0 } | | ||
| Sort-Object StartTime -Descending | ||
| $windowProcess = @($candidates) | Select-Object -First 1 | ||
| if ($null -ne $windowProcess) { | ||
| return $windowProcess | ||
| } | ||
| Start-Sleep -Milliseconds 300 | ||
| } | ||
| return $null | ||
| } | ||
|
|
||
| $logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log" | ||
| Remove-Item -LiteralPath $logPath -Force -ErrorAction SilentlyContinue | ||
| Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force | ||
|
|
||
| Write-Host "== Windows OS hotkey hook smoke ==" | ||
| $env:OPENLESS_SHOW_MAIN_ON_START = "1" | ||
| $env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS = "1" | ||
| try { | ||
| Start-Process -FilePath $ExePath -WorkingDirectory (Split-Path $ExePath -Parent) | Out-Null | ||
| } finally { | ||
| Remove-Item Env:OPENLESS_SHOW_MAIN_ON_START -ErrorAction SilentlyContinue | ||
| Remove-Item Env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS -ErrorAction SilentlyContinue | ||
| } | ||
|
|
||
| $notepad = $null | ||
| try { | ||
| if (-not (Wait-LogPattern $logPath "hotkey listener installed|Windows low-level keyboard hook" $TimeoutSeconds)) { | ||
| throw "Windows low-level keyboard hook was not installed within $TimeoutSeconds seconds." | ||
| } | ||
|
|
||
| $notepadStart = Get-Date | ||
| Start-Process notepad.exe | Out-Null | ||
| $notepad = Wait-ProcessWindow "notepad" $notepadStart 15 | ||
| if (-not (Focus-Window $notepad)) { | ||
| throw "Notepad window could not be focused." | ||
| } | ||
|
|
||
| $observedPress = $false | ||
| for ($attempt = 1; $attempt -le 3 -and -not $observedPress; $attempt++) { | ||
| Send-KeyEdge $VirtualKey $false | ||
| $observedPress = Wait-LogPattern $logPath "\[hotkey\] Windows trigger pressed" 4 | ||
| Start-Sleep -Milliseconds 400 | ||
| Send-KeyEdge $VirtualKey $true | ||
| if (-not $observedPress) { | ||
| Start-Sleep -Milliseconds 500 | ||
| Focus-Window $notepad | Out-Null | ||
| } | ||
| } | ||
|
|
||
| if (-not $observedPress) { | ||
| throw "Windows hook did not observe synthetic vk=$VirtualKey press." | ||
| } | ||
| if (-not (Wait-LogPattern $logPath "\[coord\] hotkey pressed" $TimeoutSeconds)) { | ||
| throw "Coordinator did not observe OS hook hotkey press." | ||
| } | ||
| Write-Host "[ok] Windows low-level hook observed vk=$VirtualKey and reached Coordinator." | ||
| } finally { | ||
| if ($null -ne $notepad) { | ||
| Stop-Process -Id $notepad.Id -Force -ErrorAction SilentlyContinue | ||
| } | ||
| Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force | ||
| } | ||
|
|
||
| Write-Host "Windows OS hotkey hook smoke passed." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -419,6 +419,7 @@ mod platform { | |
| const VK_RMENU: u32 = 0xA5; | ||
| const VK_RWIN: u32 = 0x5C; | ||
| const LLKHF_INJECTED: u32 = 0x0000_0010; | ||
| const ACCEPT_INJECTED_ENV: &str = "OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS"; | ||
|
|
||
| static HOOK_CONTEXT: AtomicPtr<CallbackContext> = AtomicPtr::new(std::ptr::null_mut()); | ||
|
|
||
|
|
@@ -531,7 +532,7 @@ mod platform { | |
| if code == HC_ACTION as i32 && lparam.0 != 0 { | ||
| if let Some(ctx) = callback_context() { | ||
| let keyboard = *(lparam.0 as *const KBDLLHOOKSTRUCT); | ||
| if keyboard.flags.0 & LLKHF_INJECTED == 0 { | ||
| if keyboard.flags.0 & LLKHF_INJECTED == 0 || accept_injected_events() { | ||
| dispatch_keyboard_event(ctx, keyboard.vkCode, wparam.0); | ||
| } | ||
| } | ||
|
Comment on lines
+535
to
538
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (performance): Consider caching the environment flag instead of re-reading it for every keyboard event. This hook runs on every keyboard event, so repeatedly calling Suggested implementation: const VK_RWIN: u32 = 0x5C;
const LLKHF_INJECTED: u32 = 0x0000_0010;
const ACCEPT_INJECTED_ENV: &str = "OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS";
/// Cached value of whether we should accept synthetic/injected keyboard events.
///
/// This is initialized once from the `OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS`
/// environment variable the first time `accept_injected_events()` is called,
/// and then read cheaply on every subsequent keyboard hook invocation.
static ACCEPT_INJECTED_EVENTS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
static HOOK_CONTEXT: AtomicPtr<CallbackContext> = AtomicPtr::new(std::ptr::null_mut());fn accept_injected_events() -> bool {
// Initialize once from the environment and reuse the cached value.
*ACCEPT_INJECTED_EVENTS.get_or_init(|| {
std::env::var(ACCEPT_INJECTED_ENV)
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
})
}
|
||
|
|
@@ -564,12 +565,14 @@ mod platform { | |
| WM_KEYDOWN | WM_SYSKEYDOWN => { | ||
| let was_held = ctx.shared.trigger_held.swap(true, Ordering::SeqCst); | ||
| if !was_held { | ||
| log::info!("[hotkey] Windows trigger pressed vk={vk_code}"); | ||
| send_or_log(&ctx.tx, HotkeyEvent::Pressed); | ||
| } | ||
| } | ||
| WM_KEYUP | WM_SYSKEYUP => { | ||
| let was_held = ctx.shared.trigger_held.swap(false, Ordering::SeqCst); | ||
| if was_held { | ||
| log::info!("[hotkey] Windows trigger released vk={vk_code}"); | ||
| send_or_log(&ctx.tx, HotkeyEvent::Released); | ||
| } | ||
| } | ||
|
|
@@ -593,6 +596,10 @@ mod platform { | |
| HotkeyTrigger::Fn => VK_RCONTROL, | ||
| } | ||
| } | ||
|
|
||
| fn accept_injected_events() -> bool { | ||
| std::env::var(ACCEPT_INJECTED_ENV).ok().as_deref() == Some("1") | ||
| } | ||
| } | ||
|
|
||
| // ─────────────────────────── Linux / other implementation ─────────────────────────── | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk):
Send-KeyEdgeuses a fixed scan code that may not match the provided virtual key.The helper always uses scan code
0x1D(Control) when callingkeybd_event, which only matches the default$Vk = 0xA3. If callers pass a different virtual key, the scan code/virtual key pair becomes inconsistent. Consider deriving the scan code from the virtual key (e.g., viaMapVirtualKey) or exposing the scan code as a parameter so they always match.