Moved from wayBetter for X; an app passes its own origin, process name, scripts and fixtures.
137 lines
7.1 KiB
PowerShell
137 lines
7.1 KiB
PowerShell
# Graceful-close regression check (Windows): the app must exit on a plain `taskkill /PID` (WM_CLOSE,
|
|
# no /F) in every window state, and on a simulated sign-out (WM_QUERYENDSESSION + WM_ENDSESSION).
|
|
#
|
|
# Why: tao's thread event target and the single-instance plugin's message window are visible 1 px
|
|
# unowned top-level windows. taskkill closes only the first such window in z-order; a minimized
|
|
# window sinks below them, so before the fix the close destroyed a helper and the app stayed alive.
|
|
#
|
|
# powershell -ExecutionPolicy Bypass -File dev-tools/close-check.ps1 -Exe <the app's release exe> [-Cases normal,minimized,...]
|
|
#
|
|
# Cases: normal, minimized, maximized,
|
|
# unfocused (another window in front), debugport (WebView2 remote debugging on), endsession
|
|
# (minimized, then the sign-out messages). Every case restarts the app with the owner's saved
|
|
# layout (tabs, deck columns); "webviews" counts the shell plus every tab/column webview. Any
|
|
# running copy of the same exe is closed first; a copy that does not exit is only force-killed when
|
|
# its path is this exe. Relaunch the app normally afterwards.
|
|
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Exe,
|
|
[string[]]$Cases = @('normal', 'minimized', 'maximized', 'unfocused', 'debugport', 'endsession'),
|
|
[int]$StartupSeconds = 10,
|
|
[int]$ExitSeconds = 10
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$Exe = (Resolve-Path $Exe).Path
|
|
$ProcessName = [IO.Path]::GetFileNameWithoutExtension($Exe)
|
|
|
|
Add-Type @"
|
|
using System; using System.Text; using System.Runtime.InteropServices; using System.Collections.Generic;
|
|
public static class CloseCheckWin {
|
|
public delegate bool EnumProc(IntPtr h, IntPtr l);
|
|
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc p, IntPtr l);
|
|
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
[DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder s, int n);
|
|
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);
|
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
[DllImport("user32.dll")] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, IntPtr w, IntPtr l, uint flags, uint ms, out IntPtr r);
|
|
public static List<IntPtr> Windows(uint pid) {
|
|
var r = new List<IntPtr>();
|
|
EnumWindows((h, l) => { uint p; GetWindowThreadProcessId(h, out p); if (p == pid) r.Add(h); return true; }, IntPtr.Zero);
|
|
return r;
|
|
}
|
|
[DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr h, EnumProc p, IntPtr l);
|
|
public static int CountChildren(IntPtr parent, string cls) {
|
|
int n = 0;
|
|
EnumChildWindows(parent, (h, l) => { if (ClassOf(h) == cls) n++; return true; }, IntPtr.Zero);
|
|
return n;
|
|
}
|
|
public static string ClassOf(IntPtr h) { var c = new StringBuilder(256); GetClassName(h, c, 256); return c.ToString(); }
|
|
}
|
|
"@
|
|
|
|
function Get-Ours { Get-Process $ProcessName -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $Exe } }
|
|
|
|
function Describe-Windows([int]$ProcessId) {
|
|
([CloseCheckWin]::Windows([uint32]$ProcessId) | Where-Object { [CloseCheckWin]::IsWindowVisible($_) } | ForEach-Object {
|
|
$class = [CloseCheckWin]::ClassOf($_)
|
|
if ([CloseCheckWin]::IsIconic($_)) { "$class(min)" } else { $class }
|
|
}) -join ' > '
|
|
}
|
|
|
|
function Stop-Ours {
|
|
foreach ($p in @(Get-Ours)) {
|
|
& taskkill.exe /PID $p.Id 2>&1 | Out-Null
|
|
if (-not $p.WaitForExit($ExitSeconds * 1000)) {
|
|
Write-Host " (force-killing leftover $($p.Id) at $($p.Path))"
|
|
Stop-Process -Id $p.Id -Force
|
|
$p.WaitForExit(5000) | Out-Null
|
|
}
|
|
}
|
|
}
|
|
|
|
function Start-Ours([hashtable]$Vars = @{}) {
|
|
$saved = @{}
|
|
foreach ($key in $Vars.Keys) { $saved[$key] = [Environment]::GetEnvironmentVariable($key); [Environment]::SetEnvironmentVariable($key, $Vars[$key]) }
|
|
try { Start-Process $Exe -WindowStyle Normal } finally {
|
|
foreach ($key in $saved.Keys) { [Environment]::SetEnvironmentVariable($key, $saved[$key]) }
|
|
}
|
|
$deadline = (Get-Date).AddSeconds(30)
|
|
do {
|
|
Start-Sleep -Milliseconds 500
|
|
$p = Get-Ours | Select-Object -First 1
|
|
$main = if ($p) { [CloseCheckWin]::Windows([uint32]$p.Id) | Where-Object { [CloseCheckWin]::ClassOf($_) -eq 'Tauri Window' -and [CloseCheckWin]::IsWindowVisible($_) } | Select-Object -First 1 }
|
|
} until ($main -or (Get-Date) -gt $deadline)
|
|
if (-not $main) { throw "the app window did not appear" }
|
|
Start-Sleep -Seconds $StartupSeconds # tabs, deck columns and plugins load
|
|
return @{ Process = $p; Main = $main }
|
|
}
|
|
|
|
function Wait-Exit($p) {
|
|
$watch = [Diagnostics.Stopwatch]::StartNew()
|
|
$exited = $p.WaitForExit($ExitSeconds * 1000)
|
|
return @{ Exited = $exited; Seconds = [math]::Round($watch.Elapsed.TotalSeconds, 1) }
|
|
}
|
|
|
|
$results = @()
|
|
Stop-Ours
|
|
foreach ($case in $Cases) {
|
|
$extra = if ($case -eq 'debugport') { @{ WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = '--remote-debugging-port=9333' } } else { @{} }
|
|
$app = Start-Ours $extra
|
|
$p = $app.Process; $main = $app.Main
|
|
switch ($case) {
|
|
'minimized' { [CloseCheckWin]::ShowWindow($main, 6) | Out-Null }
|
|
'endsession' { [CloseCheckWin]::ShowWindow($main, 6) | Out-Null }
|
|
'maximized' { [CloseCheckWin]::ShowWindow($main, 3) | Out-Null }
|
|
'unfocused' {
|
|
[CloseCheckWin]::ShowWindow($main, 9) | Out-Null
|
|
$shell = Get-Process explorer -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
if ($shell) { [CloseCheckWin]::SetForegroundWindow($shell.MainWindowHandle) | Out-Null }
|
|
}
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
$order = Describe-Windows $p.Id
|
|
$webviews = [CloseCheckWin]::CountChildren($main, 'WRY_WEBVIEW')
|
|
if ($case -eq 'endsession') {
|
|
# What Windows sends every top-level window at sign-out (ENDSESSION_LOGOFF).
|
|
$r = [IntPtr]::Zero
|
|
foreach ($h in [CloseCheckWin]::Windows([uint32]$p.Id)) { [CloseCheckWin]::SendMessageTimeout($h, 0x11, [IntPtr]::Zero, [IntPtr]0x80000000, 2, 2000, [ref]$r) | Out-Null }
|
|
foreach ($h in [CloseCheckWin]::Windows([uint32]$p.Id)) { [CloseCheckWin]::SendMessageTimeout($h, 0x16, [IntPtr]1, [IntPtr]0x80000000, 2, 2000, [ref]$r) | Out-Null }
|
|
} else {
|
|
& taskkill.exe /PID $p.Id 2>&1 | Out-Null
|
|
}
|
|
$outcome = Wait-Exit $p
|
|
$leftover = if ($outcome.Exited) { '' } else { Describe-Windows $p.Id }
|
|
$pass = $outcome.Exited
|
|
$results += [pscustomobject]@{ Case = $case; Pass = $pass; Seconds = $outcome.Seconds; Webviews = $webviews; WindowsBefore = $order; Leftover = $leftover }
|
|
Write-Host ('{0} {1,-16} exited={2} in {3}s webviews={5} [{4}]' -f ($(if ($pass) { 'ok ' } else { 'FAIL' })), $case, $outcome.Exited, $outcome.Seconds, $order, $webviews)
|
|
if (-not $pass) { Write-Host " still open: $leftover"; Stop-Ours }
|
|
}
|
|
|
|
$failed = @($results | Where-Object { -not $_.Pass })
|
|
Write-Host ''
|
|
Write-Host ("{0}/{1} close cases passed" -f ($results.Count - $failed.Count), $results.Count)
|
|
if ($failed.Count) { exit 1 }
|
|
exit 0
|