# 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 [-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 Windows(uint pid) { var r = new List(); 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