test(windows): 增加低层热键钩子回归 (#44)

Co-authored-by: Cooper-X-Oak <Cooper-X-Oak@users.noreply.github.com>
This commit is contained in:
Cooper OakandCooper-X-Oak authored and GitHub committed 2026-04-30 14:16:03 +08:00
1 parent 94c8507b5e
commit 3d6870822f
2 files changed
+152 -1

No files matched your search

@@ -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."
+8 -1
View File
@@ -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);
}
}
@@ -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 ───────────────────────────