Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 108 additions & 15 deletions openless-all/app/scripts/windows-capsule-lifecycle-smoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,59 @@ if (-not (Test-Path $ExePath)) {
}

$logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log"
$existingOpenLess = @(Get-Process openless -ErrorAction SilentlyContinue)
foreach ($existingProcess in $existingOpenLess) {
Stop-Process -Id $existingProcess.Id -Force -ErrorAction SilentlyContinue
}
if ($existingOpenLess.Count -gt 0) {
Start-Sleep -Milliseconds 300
}
Remove-Item -LiteralPath $logPath -Force -ErrorAction SilentlyContinue
Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force

Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;

public static class OpenLessCapsuleProbe {
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindowW(string lpClassName, string lpWindowName);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWindowVisible(IntPtr hWnd);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount);

private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

public static IntPtr FindVisibleCapsuleWindowForProcess(int processId) {
var result = IntPtr.Zero;
EnumWindows((hWnd, _) => {
if (!IsWindowVisible(hWnd)) {
return true;
}
uint ownerPid;
GetWindowThreadProcessId(hWnd, out ownerPid);
if (ownerPid != (uint)processId) {
return true;
}
var title = new StringBuilder(256);
GetWindowText(hWnd, title, title.Capacity);
if (title.ToString() == "OpenLess Capsule") {
result = hWnd;
return false;
}
return true;
}, IntPtr.Zero);
return result;
}

[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, UIntPtr dwExtraInfo);

Expand All @@ -49,16 +87,57 @@ function Wait-LogPattern($Pattern, $TimeoutSeconds) {
return $false
}

function Get-LogCount($Pattern) {
if (-not (Test-Path $logPath)) {
return 0
}
return ([regex]::Matches((Get-Content -Raw $logPath), $Pattern)).Count
}

function Get-KeyScanCode($Vk) {
switch ([int]$Vk) {
0xA0 { return 0x2A }
0xA1 { return 0x36 }
0xA2 { return 0x1D }
0xA3 { return 0x1D }
0xA4 { return 0x38 }
0xA5 { return 0x38 }
0x5B { return 0x5B }
0x5C { return 0x5C }
default { return 0 }
}
}

function Test-KeyExtended($Vk) {
return @(
0xA3, 0xA5, 0x5B, 0x5C
) -contains [int]$Vk
}

function Send-KeyEdge([byte]$Vk, [bool]$KeyUp) {
$flags = [OpenLessCapsuleProbe]::KEYEVENTF_EXTENDEDKEY
$flags = 0
if (Test-KeyExtended $Vk) {
$flags = $flags -bor [OpenLessCapsuleProbe]::KEYEVENTF_EXTENDEDKEY
}
if ($KeyUp) {
$flags = $flags -bor [OpenLessCapsuleProbe]::KEYEVENTF_KEYUP
}
[OpenLessCapsuleProbe]::keybd_event($Vk, 0x1D, $flags, [UIntPtr]::Zero)
[OpenLessCapsuleProbe]::keybd_event(
$Vk,
[byte](Get-KeyScanCode $Vk),
$flags,
[UIntPtr]::Zero
)
}

function Release-AllModifiers() {
foreach ($vk in @(0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x5B, 0x5C)) {
Send-KeyEdge $vk $true
}
}

function Get-CapsuleWindowState() {
$hwnd = [OpenLessCapsuleProbe]::FindWindowW($null, "OpenLess Capsule")
function Get-CapsuleWindowState($ProcessId) {
$hwnd = [OpenLessCapsuleProbe]::FindVisibleCapsuleWindowForProcess($ProcessId)
if ($hwnd -eq [IntPtr]::Zero) {
return [pscustomobject]@{
Exists = $false
Expand All @@ -75,7 +154,6 @@ function Get-CapsuleWindowState() {
}

Write-Host "== Windows capsule lifecycle smoke =="
$env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS = "1"
$env:OPENLESS_HOTKEY_INJECTION_DRY_RUN = "1"
$process = Start-Process -FilePath $ExePath -WorkingDirectory (Split-Path $ExePath -Parent) -PassThru
try {
Expand All @@ -84,21 +162,34 @@ try {
}

Start-Sleep -Milliseconds 500
$before = Get-CapsuleWindowState
$before = Get-CapsuleWindowState $process.Id

Send-KeyEdge 0xA3 $false
Start-Sleep -Milliseconds 120
Send-KeyEdge 0xA3 $true

$startedDryRun = Wait-LogPattern "session started \(hotkey-injection dry-run\)" 5
$startedDryRun = Wait-LogPattern "session started \(hotkey-injection dry-run\)" $TimeoutSeconds
Start-Sleep -Milliseconds 400
$afterStart = Get-CapsuleWindowState
$afterStart = Get-CapsuleWindowState $process.Id

Send-KeyEdge 0xA3 $false
Start-Sleep -Milliseconds 120
Send-KeyEdge 0xA3 $true
Start-Sleep -Seconds 3
$afterStop = Get-CapsuleWindowState
$afterStop = Get-CapsuleWindowState $process.Id

# Auto/hold semantics depend on the user's persisted mode. If the first short
# cycle was interpreted as a long hold, it already stopped the first session;
# the second cycle may therefore have started a new one. Close only that
# observed extra dry-run session, while still failing on a single-session hide
# regression instead of masking it with another key press.
if ($afterStop.Visible -and (Get-LogCount "session started \(hotkey-injection dry-run\)") -gt 1) {
Send-KeyEdge 0xA3 $false
Start-Sleep -Milliseconds 120
Send-KeyEdge 0xA3 $true
Start-Sleep -Seconds 3
$afterStop = Get-CapsuleWindowState $process.Id
}

[pscustomobject]@{
StartedDryRun = $startedDryRun
Expand All @@ -122,7 +213,9 @@ try {
Write-Host "[ok] Capsule window is not visible after synthetic stop."
}
finally {
Remove-Item Env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS -ErrorAction SilentlyContinue
Release-AllModifiers
Remove-Item Env:OPENLESS_HOTKEY_INJECTION_DRY_RUN -ErrorAction SilentlyContinue
Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force
if ($null -ne $process) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
}
157 changes: 138 additions & 19 deletions openless-all/app/scripts/windows-hotkey-os-hook-smoke.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
param(
[string]$ExePath = "",
[int]$TimeoutSeconds = 20,
[int]$VirtualKey = 0xA3
[int]$VirtualKey = 0xA3,
[int]$Iterations = 20
)

$ErrorActionPreference = "Stop"
Expand Down Expand Up @@ -36,6 +37,57 @@ public static class OpenLessInput {
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, UIntPtr dwExtraInfo);

[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

[StructLayout(LayoutKind.Sequential)]
private struct MOUSEINPUT {
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public UIntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT {
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public UIntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Explicit)]
private struct INPUT_UNION {
[FieldOffset(0)] public MOUSEINPUT mi;
[FieldOffset(0)] public KEYBDINPUT ki;
}

[StructLayout(LayoutKind.Sequential)]
private struct INPUT {
public uint type;
public INPUT_UNION U;
}

public static uint SendInputKey(byte bVk, bool keyUp) {
var extended = bVk == 0xA3 || bVk == 0xA5 || bVk == 0x5B || bVk == 0x5C;
var input = new INPUT {
type = 1,
U = new INPUT_UNION {
ki = new KEYBDINPUT {
wVk = bVk,
wScan = 0,
dwFlags = (uint)((keyUp ? KEYEVENTF_KEYUP : 0) | (extended ? KEYEVENTF_EXTENDEDKEY : 0)),
time = 0,
dwExtraInfo = UIntPtr.Zero
}
}
};
return SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT)));
}

public const int KEYEVENTF_EXTENDEDKEY = 0x0001;
public const int KEYEVENTF_KEYUP = 0x0002;
}
Expand All @@ -55,12 +107,73 @@ function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) {
return $false
}

function Send-KeyEdge($Vk, $KeyUp) {
$flags = [OpenLessInput]::KEYEVENTF_EXTENDEDKEY
function Get-KeyScanCode($Vk) {
switch ([int]$Vk) {
0xA0 { return 0x2A }
0xA1 { return 0x36 }
0xA2 { return 0x1D }
0xA3 { return 0x1D }
0xA4 { return 0x38 }
0xA5 { return 0x38 }
0x5B { return 0x5B }
0x5C { return 0x5C }
default { return 0 }
}
}

function Test-KeyExtended($Vk) {
return @(
0xA3, 0xA5, 0x5B, 0x5C
) -contains [int]$Vk
}

function Send-KeyEdge($Vk, $KeyUp, [ValidateSet("keybd_event", "SendInput")] [string]$Method) {
if ($Method -eq "SendInput") {
if ([OpenLessInput]::SendInputKey([byte]$Vk, [bool]$KeyUp) -ne 1) {
throw "SendInput failed for vk=$Vk keyUp=$KeyUp (Win32=$([Runtime.InteropServices.Marshal]::GetLastWin32Error()))."
}
return
}

$flags = 0
if (Test-KeyExtended $Vk) {
$flags = $flags -bor [OpenLessInput]::KEYEVENTF_EXTENDEDKEY
}
if ($KeyUp) {
$flags = $flags -bor [OpenLessInput]::KEYEVENTF_KEYUP
}
[OpenLessInput]::keybd_event([byte]$Vk, 0x1D, $flags, [UIntPtr]::Zero)
[OpenLessInput]::keybd_event(
[byte]$Vk,
[byte](Get-KeyScanCode $Vk),
$flags,
[UIntPtr]::Zero
)
}

function Release-AllModifiers() {
foreach ($vk in @(0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x5B, 0x5C)) {
# keybd_event 的 key-up 可清理由两种注入 API 设置的系统修饰键状态;
# 重复 key-up 是幂等的,适合在失败路径兜底。
Send-KeyEdge $vk $true "keybd_event"
}
}

function Get-LogCount($Path, $Pattern) {
if (-not (Test-Path $Path)) {
return 0
}
return ([regex]::Matches((Get-Content -Raw $Path), $Pattern)).Count
}

function Wait-LogCount($Path, $Pattern, $Minimum, $TimeoutSeconds) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if ((Get-LogCount $Path $Pattern) -ge $Minimum) {
return $true
}
Start-Sleep -Milliseconds 250
}
return $false
}

function Focus-Window($Process) {
Expand Down Expand Up @@ -94,12 +207,10 @@ 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
Expand All @@ -115,26 +226,34 @@ try {
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
$methods = @("keybd_event", "SendInput")
foreach ($method in $methods) {
$pressedBefore = Get-LogCount $logPath "\[hotkey\] Windows trigger pressed"
$releasedBefore = Get-LogCount $logPath "\[hotkey\] Windows trigger released"
Write-Host "Testing $method with $Iterations complete down/up cycles..."

for ($iteration = 1; $iteration -le $Iterations; $iteration++) {
Send-KeyEdge $VirtualKey $false $method
Start-Sleep -Milliseconds 35
Send-KeyEdge $VirtualKey $true $method
Start-Sleep -Milliseconds 35
}
}

if (-not $observedPress) {
throw "Windows hook did not observe synthetic vk=$VirtualKey press."
if (-not (Wait-LogCount $logPath "\[hotkey\] Windows trigger pressed" ($pressedBefore + $Iterations) $TimeoutSeconds)) {
throw "$method did not produce $Iterations Windows trigger pressed events."
}
if (-not (Wait-LogCount $logPath "\[hotkey\] Windows trigger released" ($releasedBefore + $Iterations) $TimeoutSeconds)) {
throw "$method did not produce $Iterations Windows trigger released events."
}
Write-Host "[ok] $method produced $Iterations complete hotkey cycles."
}

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."
Write-Host "[ok] Windows low-level hook accepted keybd_event and SendInput for vk=$VirtualKey."
} finally {
Release-AllModifiers
if ($null -ne $notepad) {
Stop-Process -Id $notepad.Id -Force -ErrorAction SilentlyContinue
}
Expand Down
Loading
Loading