From 3a13e562492fd7f4b158512a7fe690bd00194f61 Mon Sep 17 00:00:00 2001 From: Cooper-X-Oak Date: Thu, 30 Apr 2026 11:59:01 +0800 Subject: [PATCH] =?UTF-8?q?test(windows):=20=E5=A2=9E=E5=8A=A0=E4=BD=8E?= =?UTF-8?q?=E5=B1=82=E7=83=AD=E9=94=AE=E9=92=A9=E5=AD=90=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/windows-hotkey-os-hook-smoke.ps1 | 144 ++++++++++++++++++ openless-all/app/src-tauri/src/hotkey.rs | 9 +- 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 openless-all/app/scripts/windows-hotkey-os-hook-smoke.ps1 diff --git a/openless-all/app/scripts/windows-hotkey-os-hook-smoke.ps1 b/openless-all/app/scripts/windows-hotkey-os-hook-smoke.ps1 new file mode 100644 index 00000000..ae9a8fc2 --- /dev/null +++ b/openless-all/app/scripts/windows-hotkey-os-hook-smoke.ps1 @@ -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." diff --git a/openless-all/app/src-tauri/src/hotkey.rs b/openless-all/app/src-tauri/src/hotkey.rs index cb7fcf17..39522ae1 100644 --- a/openless-all/app/src-tauri/src/hotkey.rs +++ b/openless-all/app/src-tauri/src/hotkey.rs @@ -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 = 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 ───────────────────────────