From 4e0478c21adc0dd981d89f0f96159feedc3b22e6 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 9 Aug 2026 21:04:39 +0200 Subject: [PATCH] bootcheck: reap a QEMU left behind by a killed run The `finally` stops QEMU on every path the script controls, but not when the script itself is killed -- a CI timeout, Ctrl-C at the wrong moment, pkill. QEMU then keeps running with no parent. Because these tests run under TCG rather than KVM, a spare emulator pinning a core slows every later run, and one was found still executing the guest at 86% of a core eight minutes after its parent was gone. That is the bad kind of failure: the suite does not break, it gets slower, and commands drift past Wait-Prompt's timeout until the next one is typed into a busy shell and the transcript interleaves. The check that then fails is unrelated to whatever actually changed. Record the pid on startup and reap the previous run's leftover before starting a new one, the same self-healing approach already used for the monitor port. The name is checked before killing, because pids are recycled and killing an unrelated process would be worse than the leak. Co-Authored-By: Claude Opus 5 --- tools/windows/bootcheck.ps1 | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tools/windows/bootcheck.ps1 b/tools/windows/bootcheck.ps1 index 8ad3c6f..50cd2da 100644 --- a/tools/windows/bootcheck.ps1 +++ b/tools/windows/bootcheck.ps1 @@ -85,9 +85,39 @@ $hda = Join-Path $root 'schos.img' # choice -- a kernel handed anything else on device 1 panics in forkret(). $hdb = Join-Path $root 'build/schfs.img' $log = Join-Path $root 'build/bootcheck.log' +# Where the running QEMU's pid is recorded, so the *next* run can clean up after +# one that never got to its `finally` -- see reap_stale_qemu below. +$pidfile = Join-Path $root 'build/bootcheck-qemu.pid' Remove-Item -Force $log -ErrorAction SilentlyContinue +# Kill a QEMU left over from a previous run. +# +# The `finally` at the bottom stops QEMU on every path the script controls, but +# not when the script itself is killed -- a CI timeout, Ctrl-C at the wrong +# moment, `pkill pwsh`. QEMU then keeps running with no parent, and because +# these tests run under TCG rather than KVM, a spare emulator pinning a core +# makes every *later* run slower. Commands that normally answer in a second +# drift past Wait-Prompt's timeout, the next command is typed into a busy +# shell, and the transcript interleaves -- which surfaces as a failed check +# somewhere unrelated to whatever actually changed. A test environment that +# silently degrades is worse than one that fails loudly, so each run reaps the +# last one's leftovers before starting. +function Reap-StaleQemu { + if (-not (Test-Path $pidfile)) { return } + $stale = (Get-Content $pidfile -Raw -ErrorAction SilentlyContinue).Trim() + Remove-Item -Force $pidfile -ErrorAction SilentlyContinue + if (-not $stale) { return } + $proc = Get-Process -Id ([int]$stale) -ErrorAction SilentlyContinue + # Check the name too: pids are recycled, and killing an unrelated process + # because it inherited the number would be a far worse bug than the leak. + if ($proc -and $proc.ProcessName -like 'qemu*') { + Write-Host " reaping leaked QEMU from a previous run (pid $stale)" -ForegroundColor Yellow + try { $proc.Kill() } catch { } + } +} +Reap-StaleQemu + # The default run exercises the *root* filesystem: create a file, read it back, # grow it, list a directory, seek around it. Booting to a prompt proves nothing # on its own, and every one of these has broken at some point while the boot @@ -274,6 +304,9 @@ $psi.UseShellExecute = $false $what = if ($Iso) { 'schos.iso, no hard disk' } else { "hdb = $(Split-Path $hdb -Leaf)" } Write-Host "==> booting ($what)" -ForegroundColor Cyan $p = [System.Diagnostics.Process]::Start($psi) +# Recorded immediately, so that a run killed one instruction later is still +# cleaned up by the next one. +Set-Content -Path $pidfile -Value $p.Id try { Start-Sleep -Seconds $BootSeconds # wait for the shell prompt @@ -401,6 +434,7 @@ try { } finally { if (-not $p.HasExited) { $p.Kill() } Start-Sleep -Milliseconds 800 + Remove-Item -Force $pidfile -ErrorAction SilentlyContinue } if (-not (Test-Path $log)) { throw "no serial log produced at $log" }