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 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-09 21:04:39 +02:00
commit 4e0478c21a

View file

@ -85,9 +85,39 @@ $hda = Join-Path $root 'schos.img'
# choice -- a kernel handed anything else on device 1 panics in forkret(). # choice -- a kernel handed anything else on device 1 panics in forkret().
$hdb = Join-Path $root 'build/schfs.img' $hdb = Join-Path $root 'build/schfs.img'
$log = Join-Path $root 'build/bootcheck.log' $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 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, # 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 # 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 # 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)" } $what = if ($Iso) { 'schos.iso, no hard disk' } else { "hdb = $(Split-Path $hdb -Leaf)" }
Write-Host "==> booting ($what)" -ForegroundColor Cyan Write-Host "==> booting ($what)" -ForegroundColor Cyan
$p = [System.Diagnostics.Process]::Start($psi) $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 { try {
Start-Sleep -Seconds $BootSeconds # wait for the shell prompt Start-Sleep -Seconds $BootSeconds # wait for the shell prompt
@ -401,6 +434,7 @@ try {
} finally { } finally {
if (-not $p.HasExited) { $p.Kill() } if (-not $p.HasExited) { $p.Kill() }
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
Remove-Item -Force $pidfile -ErrorAction SilentlyContinue
} }
if (-not (Test-Path $log)) { throw "no serial log produced at $log" } if (-not (Test-Path $log)) { throw "no serial log produced at $log" }