SchOS/tools/windows/bootcheck.ps1
Nexoniarz baede13847 schinit: a launchd-shaped process 1
Milestone 1 of three: manifests, RunAtLoad, KeepAlive with throttling,
and reaping. Replaces the hardcoded fork-a-shell loop in cmd/init, which
stays in the tree for now.

Jobs are sectioned key=value files in /etc/schinit/*.job, kept in the
repo under etc/ and installed into the sysroot -- sysroot/ is generated
and gitignored, so anything written only there is lost on a fresh clone,
which is already true of /etc/passwd and should not become true of the
boot configuration. Unknown keys and sections are ignored rather than
fatal: a manifest written for a newer schinit must not stop this one
booting the machine.

Socket sections are parsed and recorded but not yet held. That is
milestone 2 and it is the whole reason for copying launchd rather than
sysvinit -- the supervisor holds the listener and starts the job on first
connect, which inverts the dependency graph and makes runlevels a
non-concept. The key exists now so manifests need not change later.

getdents64 is exposed to userland for this. The kernel has had it since
the Linux ABI landed, but nothing declared it in user.h or lib/usys.S, so
first-party programs had no way to list a directory at all.

Two things found by testing rather than reasoning:

  - setsid() gives a job its own session, and a session has no
    controlling terminal. The console then delivers input only to the
    foreground process group it tracks, so the shell came up, echoed
    every keystroke through the line discipline, and executed none of
    them -- indistinguishable from a hung shell. ControllingTerminal=yes
    claims it with TIOCSCTTY/TIOCSPGRP; off by default, since a daemon
    has no business seizing the console.
  - the first bootstrap() dropped /dev/input/event0-1 and most of /proc,
    because it was transcribed from the part of the old init that had
    been read rather than all of it. -Evdev caught it.

build/usbstick.img is built by `all` now: without it QEMU refuses the
-drive and the entire -Usb run produces no output, which reads as a
broken USB driver.

Seven suites green: base 27, Tty 51, Ps 32, Sock 64, Fb 36, Usb 31,
Evdev 29. make schfs-check passes.
2026-08-09 16:36:13 +02:00

553 lines
26 KiB
PowerShell

# Headless boot check: run SchOS under QEMU, type commands at the shell, and
# assert on what actually comes back.
#
# Booting to a prompt proves nothing on its own -- a filesystem can mount and
# still fail on the first real command, and exactly that has happened here
# before. So this drives the serial console and greps the transcript for the
# answers those commands should produce.
#
# The console is stdio with a chardev logfile: QEMU writes every byte of the
# session to a file itself, so this script only ever has to *write* to stdin.
# Reading QEMU's stdout from PowerShell needs a reader that cannot block the
# writer, and two attempts at that (a background task, then a duplex named
# pipe) both deadlocked. Letting QEMU do the logging removes the problem.
#
# QEMU's serial line has no flow control: a burst of characters gets dropped,
# and anything typed before the shell is listening is discarded outright. Hence
# the pacing below -- not superstition, just the difference between a reliable
# check and a flaky one.
#
# tools/bootcheck.ps1
# tools/bootcheck.ps1 -Tty -Ps
param(
[switch]$Fb,
# Boot schos.iso from an emulated optical drive with NO hard disk
# attached, which is the only way to prove the ISO is self-contained.
# Needs `make iso` first.
[switch]$Iso,
# Terminal behaviour: pipes, isatty, SIGPIPE, and Ctrl-C. None of these
# can be checked by looking at a screenshot, and all of them were broken
# in ways that looked like broken *commands*.
[switch]$Tty,
# Attach an xHCI controller with a keyboard, a mouse and a mass storage
# device on it. Nothing on the default machine has a USB port, so without
# this the USB driver is never asked to do anything and a broken one looks
# exactly like a working one.
[switch]$Usb,
# Unix domain sockets. Nothing in the default command set opens one, and a
# socket layer that accepted every call and dropped every byte would look
# identical from the outside -- socktest has one process write and another
# read, which is the only check that can tell the difference.
[switch]$Sock,
# Raw input events. Needs the QEMU monitor for the same reason the USB run
# does: a key has to be *pressed*, and characters typed at the serial port
# never touch the keyboard controller at all.
[switch]$Evdev,
# Process enumeration. /proc is a synthetic filesystem now, and the thing
# it exists for -- one directory per process -- is exactly what nothing
# else in this suite touches.
[switch]$Ps,
[int]$BootSeconds = 20,
[string]$Qemu
)
# QEMU, on whichever host this is.
#
# PowerShell 7 runs on Linux, macOS and the BSDs as well as Windows, so these
# scripts are usable from all of them -- but the old default was a hardcoded
# scoop path under $env:USERPROFILE, which exists on exactly one of those. PATH
# first, because a package-managed qemu is on it everywhere; the scoop location
# is only a fallback for the Windows install that does not add itself to PATH.
function Find-Qemu {
$exe = if ($IsWindows -or $env:OS -eq 'Windows_NT') { 'qemu-system-x86_64.exe' }
else { 'qemu-system-x86_64' }
$onPath = Get-Command $exe -ErrorAction SilentlyContinue
if ($onPath) { return $onPath.Source }
if ($env:USERPROFILE) {
$scoop = Join-Path (Join-Path $env:USERPROFILE 'scoop/apps/qemu/current') $exe
if (Test-Path $scoop) { return $scoop }
}
foreach ($p in @('/usr/bin', '/usr/local/bin', '/opt/homebrew/bin')) {
$c = Join-Path $p $exe
if (Test-Path $c) { return $c }
}
throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu <path>."
}
if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$hda = Join-Path $root 'schos.img'
# The root filesystem. SchFS is the only root there is, so this is not a
# 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'
Remove-Item -Force $log -ErrorAction SilentlyContinue
# 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
# still looked perfectly healthy.
$commands = @(
'echo hello-from-schos > /tmp/bc.txt'
'cat /tmp/bc.txt'
'echo second-line >> /tmp/bc.txt'
'wc -c /tmp/bc.txt'
'mkdir /tmp/bcdir'
'cp /tmp/bc.txt /tmp/bcdir/copy.txt'
'cat /tmp/bcdir/copy.txt'
'ls /tmp/bcdir'
'rm /tmp/bcdir/copy.txt'
'rmdir /tmp/bcdir'
'ls /bin | wc -l'
'cat /proc/pci'
)
# Every command above already went to SchFS. What is left is to show that
# it is SchFS they went to, and that the volume noticed: the generation
# counter must advance across the writes, which is the one thing a
# read-only mount or a silently-discarded write could not fake.
$commands += 'cat /proc/schfs'
# Mounting the root device a second time has to fail. It is the check that
# tells "the volume is genuinely mounted as /" apart from "it happens to be
# readable", and getting EBUSY here is the correct answer, not a defect.
$commands += 'mkdir /mnt'
$commands += 'mount -t schfs /dev/hdb /mnt'
# A deep path built and torn down entirely on the root volume.
$commands += 'mkdir /tmp/rootdir'
$commands += 'mkdir /tmp/rootdir/sub'
$commands += 'echo schfs-is-root > /tmp/rootdir/sub/deep.txt'
$commands += 'cat /tmp/rootdir/sub/deep.txt'
$commands += 'ls /tmp/rootdir/sub'
$commands += 'rm /tmp/rootdir/sub/deep.txt'
$commands += 'rmdir /tmp/rootdir/sub'
$commands += 'rmdir /tmp/rootdir'
# A binary loaded off the root volume, not a shell builtin: exec() reading
# an ELF through schfs_ops is the path that carried init itself.
$commands += 'wc -l /etc/motd'
# The second dump. The generation counter is compared against the first
# one: on a copy-on-write volume it moves on every committed transaction,
# so a root that silently discarded its writes would still pass every read
# above and fail here.
$commands += 'cat /proc/schfs'
# init must survive signals it has no handler for.
#
# This is a crash-the-whole-machine bug, not a nicety: Ctrl-C sends SIGINT to
# the foreground process group, init is pgid 1, and any window where
# tty_fg_pgrp is 1 lands it on init -- which had no handler, so killed was set,
# exit() ran, and exit() panics outright on initproc. Spamming Ctrl-C at a
# frozen program is what finds that window.
#
# SIGKILL is in here deliberately: init is not killable from userspace on any
# Unix, and a guard that covers SIGINT but not SIGKILL is half a fix.
$commands += 'kill -INT 1'
$commands += 'kill -KILL 1'
$commands += 'kill -TERM 1'
$commands += 'echo INIT-SURVIVED'
$commands += 'ps | head -3'
# KeepAlive. Exiting the shell must bring a new one back -- that is the
# supervision loop doing its job, and it is the only part of schinit that
# cannot be checked by reading its startup output.
$commands += 'echo BEFORE-EXIT'
$commands += 'exit'
$commands += 'echo SHELL-RESPAWNED'
if ($Fb) {
# /dev/fb0. The geometry has to come back from the mode the bootloader
# actually programmed -- three places in the kernel used to assume
# 0xfd000000 at 800x600x32, and nothing would have said when that stopped
# being true. `bars` goes through the device's write(), `fill` through
# mmap, so a failure names which of the two paths broke.
$commands += 'fbinfo'
$commands += 'fbinfo bars'
$commands += 'fbinfo fill'
}
if ($Tty) {
# `ls` piped into `wc -l` reported 48 rather than 240 because ioctl
# answered TIOCGWINSZ on a *pipe* with success, so ls formatted in
# columns. `yes` failed outright because writev capped iovcnt at 16 and
# toybox builds 256 of them. `cat | head` killed the shell, because
# SIG_IGN is handler value 1 and signal delivery jumped to address 1.
# Three different-looking bugs, none of them in the commands.
$commands += 'ls /bin | wc -l'
$commands += 'yes | head -2'
# 4 bytes, not 3: an ELF file starts with 0x7F 'E' 'L' 'F', so three bytes
# stops at "EL" and the invisible leading byte makes that easy to misread.
$commands += 'cat /bin/toybox | head -c 4; echo'
$commands += 'printf ''caf\303\251-\303\261-\303\266\n'''
$commands += 'echo TTY-SURVIVED'
# Shared memory and file-backed mmap: the two things a display server
# cannot be written without, and the two whose failure mode is a
# successful-looking return value.
$commands += 'shmtest'
}
if ($Usb) {
# /proc/interrupts is how you tell a registered handler from one that
# merely compiled. A line with a zero count is a device that was found and
# never heard from.
$commands += 'cat /proc/interrupts'
# Read a real block off the USB stick, through the buffer cache. Answering
# INQUIRY proves a device is there; only this proves it can read.
$commands += 'echo s > /proc/sysrq'
}
if ($Sock) {
$commands += 'socktest'
}
if ($Ps) {
$commands += 'ls /proc'
$commands += 'cat /proc/1/stat'
$commands += 'ps'
$commands += 'ps -A -o pid,ppid,comm'
$commands += 'top -b -n 1 | head -6'
}
$commands += 'echo MARKER-DONE'
# The ISO run attaches no hard disk at all. That is the whole point: a kernel
# that still quietly needs one would boot happily here otherwise, and only
# fail on someone else's machine.
$isoPath = Join-Path $root 'schos.iso'
if ($Iso -and -not (Test-Path $isoPath)) { throw "no $isoPath -- run 'make iso' first" }
$drives = if ($Iso) {
@("-cdrom `"$isoPath`"", '-boot d')
} else {
@("-drive file=`"$hda`",format=raw,index=0,media=disk"
"-drive file=`"$hdb`",format=raw,index=1,media=disk")
}
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu
$usbDevices = if ($Usb) {
# One device of each speed class QEMU can produce: usb-storage negotiates
# SuperSpeed on qemu-xhci, the HID devices come up high speed. They take
# different paths through enumeration -- a USB3 port arrives already
# enabled and a USB2 port has to be reset first -- so one of each is the
# difference between testing the driver and testing half of it.
@('-device qemu-xhci,id=xhci'
'-device usb-kbd,bus=xhci.0'
# The mouse goes behind a hub, so the tree is a tree and not a row of
# root ports. That exercises the route string, the hub's own slot
# context, and the transaction translator -- none of which a device
# plugged straight into the controller touches at all.
# QEMU places a device under a hub by port *path*, not by a child bus:
# the hub sits on port 2 and the mouse on port 2.1 of the same bus.
'-device usb-hub,id=hub,bus=xhci.0,port=2'
'-device usb-mouse,bus=xhci.0,port=2.1'
# Its own small image, never $hdb. The checks below read a v1 superblock
# off this stick and assert on its sector count and size field, so the
# stick has to be a v1 image whatever the root filesystem is -- under
# the root $hdb is a SchFS volume and those fields are simply not
# there, which failed three USB checks that had nothing wrong with USB.
# It also stops the same file being attached twice, once as the root
# disk and once as a USB device.
"-drive id=usbstick,file=`"$(Join-Path $root 'build/usbstick.img')`",format=raw,if=none"
'-device usb-storage,bus=xhci.0,drive=usbstick')
} else { @() }
# The USB run needs the QEMU monitor, because `sendkey` is the only way to
# press a key on an emulated USB keyboard -- typing on stdin goes to the
# serial port and proves nothing about USB.
# Ask the OS for a free port rather than hardcoding one. A run that dies
# before its `finally` leaves QEMU holding the old number, and every later run
# then fails with "Failed to find an available port" -- which looks like a
# bug in whatever is being tested rather than in the harness.
$monPort = if ($Usb -or $Evdev) {
$l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
$l.Start(); $p = $l.LocalEndpoint.Port; $l.Stop(); $p
} else { 0 }
$monitor = if ($Usb -or $Evdev) { @("-monitor tcp:127.0.0.1:$monPort,server,nowait") } else { @() }
$psi.Arguments = ($drives + $usbDevices + $monitor + @(
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none'
"-chardev stdio,id=c0,logfile=`"$log`""
'-serial chardev:c0'
)) -join ' '
$psi.RedirectStandardInput = $true
$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)
try {
Start-Sleep -Seconds $BootSeconds # wait for the shell prompt
if ($Usb) {
# Type a whole command on the *USB* keyboard. QEMU routes sendkey to
# the most recently attached keyboard, so with usb-kbd present these
# keystrokes go through xHCI, the interrupt endpoint, the HID report
# decoder and the console -- none of which the serial path touches.
# If any link in that chain is broken the characters simply vanish,
# which is exactly how the missing interrupt showed up.
Write-Host ' > (usb keyboard) echo usbworks' -ForegroundColor DarkGray
$mon = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
$monw = New-Object System.IO.StreamWriter($mon.GetStream())
$monw.AutoFlush = $true
Start-Sleep -Milliseconds 400
foreach ($k in @('e','c','h','o','spc','u','s','b','w','o','r','k','s','ret')) {
$monw.WriteLine("sendkey $k")
Start-Sleep -Milliseconds 200
}
Start-Sleep -Seconds 2
$mon.Close()
}
# Wait until the shell has finished the last command and printed a fresh
# prompt, rather than sleeping a fixed interval and hoping.
#
# A flat 4-second wait was wrong in both directions: too long for the
# dozens of commands that answer instantly, and too short for `ls /bin |
# wc -l`, which reliably overran it. When it overran, the next command was
# typed while the shell was still busy -- the tty echoed it but no reader
# took it, and it ended up concatenated onto the following one. The
# transcript showed `cat /proc/pcicat /proc/schfs` and three PCI checks
# failed, which looks exactly like a broken PCI driver and is not one.
#
# This cannot read QEMU's stdout -- see the header; two attempts at that
# deadlocked. It reads the logfile QEMU is writing instead, which is the
# same bytes with none of the plumbing.
function Wait-Prompt([int]$TimeoutMs = 20000) {
$deadline = (Get-Date).AddMilliseconds($TimeoutMs)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 150
$tail = ''
try { $tail = Get-Content $log -Raw -ErrorAction Stop } catch { continue }
if ($null -eq $tail) { continue }
# A prompt at the very end, with nothing typed after it.
if ($tail -match '(?s)#[ ]?$') { return $true }
}
return $false
}
foreach ($cmd in $commands) {
Write-Host " > $cmd" -ForegroundColor DarkGray
foreach ($ch in ($cmd + "`n").ToCharArray()) {
$p.StandardInput.Write($ch)
$p.StandardInput.Flush()
Start-Sleep -Milliseconds 30 # no flow control: pace the input
}
if (-not (Wait-Prompt)) {
Write-Host " (no prompt after '$cmd' -- continuing)" -ForegroundColor Yellow
Start-Sleep -Seconds 2
}
}
if ($Evdev) {
# Start a reader on /dev/input/event0 and then press keys at it. The
# program blocks in read() until events arrive, so if the keyboard
# driver is not feeding the input layer this hangs and prints nothing
# -- which is a failure that says exactly which half is broken.
Write-Host ' > inputtest -n 6, then press keys' -ForegroundColor DarkGray
foreach ($ch in "inputtest -n 6`n".ToCharArray()) {
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
Start-Sleep -Milliseconds 30
}
Start-Sleep -Seconds 2
$mon = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
$monw = New-Object System.IO.StreamWriter($mon.GetStream())
$monw.AutoFlush = $true
Start-Sleep -Milliseconds 400
# 'a' is keycode 30 and shift is 42, chosen because both numbers are
# unmistakable in the output and neither is a scancode by accident.
# Each sendkey is a press *and* a release, which is the whole point:
# the console would report one character and nothing else.
foreach ($k in @('a', 'shift', 'a', 'b')) {
$monw.WriteLine("sendkey $k")
Start-Sleep -Milliseconds 400
}
$mon.Close()
Start-Sleep -Seconds 3
# Those keypresses reached the console as well as the input device --
# that is the design, not a leak -- so the shell's input line now holds
# "aab". Clear it before typing the next command, or the shell tries to
# run `aabecho`.
$p.StandardInput.Write("`n"); $p.StandardInput.Flush()
Start-Sleep -Milliseconds 500
foreach ($ch in "echo INPUT-DONE`n".ToCharArray()) {
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
Start-Sleep -Milliseconds 30
}
Start-Sleep -Seconds 3
}
if ($Tty) {
# Ctrl-C has to reach the *foreground process group*. The old handler
# set killed=1 on whatever process happened to be running when the
# keyboard interrupt fired, which is almost never the one on screen.
Write-Host ' > sleep 30, then Ctrl-C' -ForegroundColor DarkGray
foreach ($ch in "sleep 30`n".ToCharArray()) {
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
Start-Sleep -Milliseconds 30
}
Start-Sleep -Seconds 2
$p.StandardInput.Write([char]3) # ^C
$p.StandardInput.Flush()
Start-Sleep -Seconds 2
foreach ($ch in "echo INTERRUPTED-OK`n".ToCharArray()) {
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
Start-Sleep -Milliseconds 30
}
Start-Sleep -Seconds 3
}
} finally {
if (-not $p.HasExited) { $p.Kill() }
Start-Sleep -Milliseconds 800
}
if (-not (Test-Path $log)) { throw "no serial log produced at $log" }
$text = Get-Content $log -Raw
if ($null -eq $text) { $text = '' }
Write-Host "==> serial transcript ($($text.Length) bytes) -> $log" -ForegroundColor Cyan
Write-Host $text
$script:fail = $false
function Expect($pattern, $what) {
if ($script:text -match $pattern) {
Write-Host " [ok] $what" -ForegroundColor Green
} else {
Write-Host " FAIL: $what (no match for /$pattern/)" -ForegroundColor Red
$script:fail = $true
}
}
Expect 'MARKER-DONE' 'shell stayed alive through every command'
Expect 'hello-from-schos' 'a file written on the root filesystem reads back'
Expect 'second-line' 'appending to that file works'
# 17 bytes of "hello-from-schos\n" plus 12 of "second-line\n". The exact
# number is the point: an append that silently truncated still produced a file,
# and only the byte count said which one.
#
# It follows the string above, so changing that changes this -- the SchOS
# rename shortened "hello-from-schunix" by two characters and this check went
# red on a filesystem that was working perfectly.
Expect '(?m)^\s*29\s' 'append extended the file rather than replacing it'
Expect '(?m)^copy\.txt' 'a copied file appears in a new directory'
Expect '(?ms)copy\.txt.*rmdir' 'rmdir removed the directory without an error'
# PCI. The point of these is that the numbers come off the bus rather than out
# of a header: the host bridge identifies the chipset QEMU is emulating, and
# the display controller's aperture is the same address VBE reported, checked
# by two completely independent paths.
Expect 'pci: \d+ devices' 'the PCI bus enumerated'
Expect '00:00\.0 8086:1237' 'the host bridge is where a host bridge belongs'
Expect '00:01\.1 8086:7010 IDE' 'the IDE controller was identified by class'
Expect '\[vga\]' 'a driver bound itself to the display controller'
Expect 'vga: display controller' 'the display driver found its aperture'
# If init had died the machine would have panicked and nothing below this
# point would appear at all -- but check explicitly, so a failure names the
# cause instead of showing up as "the shell stopped answering".
Expect 'INIT-SURVIVED' 'init survives SIGINT, SIGKILL and SIGTERM'
Expect 'schinit: ready \(pid 1\)' 'schinit came up as process 1'
Expect 'job.? loaded' 'schinit read its manifests from /etc/schinit'
Expect 'starting os\.schos\.' 'a job started by Label, not by hardcoded path'
Expect 'SHELL-RESPAWNED' 'KeepAlive restarted the shell after it exited'
Expect '(?m)^\s*1\s' 'pid 1 is still in the process table afterwards'
Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem'
Expect 'init: ready' 'init came up on a SchFS root'
Expect 'label:\s*schos' '/proc/schfs reports the volume label'
Expect 'root_objectid:\s*16' '/proc/schfs reports the root object id'
Expect 'dir\s+bin' 'the root directory really contains bin/'
# / is the mounted volume, so mounting that same device again must be
# refused. Without this a kernel that had quietly fallen back to some other
# root would still pass everything else here.
Expect 'Resource busy' 'mounting the root device twice is refused'
Expect 'schfs-is-root' 'a file three directories deep reads back'
Expect '(?m)^deep\.txt' 'that file is listed by its full name'
Expect '(?m)^\s*1\s+/etc/motd' 'a binary loaded off the SchFS root runs'
# Two /proc/schfs dumps, before and after the writes. Same volume, so the
# generation must have moved -- if it has not, nothing was committed.
$gens = [regex]::Matches($text, 'generation:\s*(\d+)')
if ($gens.Count -ge 2 -and
[int]$gens[$gens.Count-1].Groups[1].Value -gt [int]$gens[0].Groups[1].Value) {
Write-Host " [ok] the volume generation advanced across the writes" -ForegroundColor Green
} else {
Write-Host " FAIL: the volume generation did not advance -- writes were not committed" -ForegroundColor Red
$script:fail = $true
}
if ($Fb) {
Expect 'id\s+schosfb' 'FBIOGET_FSCREENINFO identifies the device'
# 800x600 is what boot/bios/stage2.S asks VBE for. Asserting the numbers rather
# than just "the ioctl returned" is the point: the old code path returned
# success with an untouched buffer, i.e. a 0x0 screen.
Expect '(?m)^xres\s+800' 'the reported width is the mode actually set'
Expect '(?m)^yres\s+600' 'the reported height is the mode actually set'
Expect '(?m)^bits_per_pixel\s+32' 'the reported depth is the mode actually set'
Expect '(?m)^smem_start\s+0x' 'the framebuffer reports a physical address'
Expect '(?m)^line_length\s+3200' 'the stride comes from VBE, not from xres*bpp'
Expect 'transp.*absent' 'the absent alpha channel is reported as length 0'
Expect 'colour bars drawn' 'writing pixels through write() reaches the screen'
Expect 'gradient drawn' 'mmap of /dev/fb0 maps the real framebuffer'
}
if ($Tty) {
# Each of these is a bug that once looked like a broken *command*.
# `ls | wc -l` reported 48 rather than the real count because ioctl
# answered TIOCGWINSZ on a pipe with success, so ls formatted in columns.
# Anything in the hundreds means it printed one name per line.
Expect '(?m)^\s*[2-9][0-9]{2}\s*$' 'ls into a pipe prints one name per line'
# writev capped iovcnt at 16; toybox builds 256 of them, so `yes` failed
# outright.
Expect '(?ms)yes \| head -2\s*\n\s*y\s*\n\s*y' 'yes writes through a pipe'
# 0x7F E L F. Four bytes, not three -- the leading byte is invisible.
Expect 'ELF' 'a binary reads back through a pipe intact'
Expect 'caf.*-.*-' 'UTF-8 survives the terminal unmangled'
# SIG_IGN is handler value 1, and signal delivery once jumped to address 1,
# which killed the shell rather than the pipeline.
Expect 'TTY-SURVIVED' 'the shell is still alive after all of that'
Expect 'shmtest: all checks passed' 'shared memory and file-backed mmap work'
}
if ($Usb) {
# A registered handler that never fires looks exactly like a working one
# from userspace. A non-zero count on the xhci line is the difference.
# Not anchored to the start of the line: xHCI shares IRQ 11 with the e1000
# here, so the line reads "11: 28 e1000, 28 xhci level" and the xhci
# entry is the second one on it.
Expect '[1-9]\d*\s+xhci' 'the xHCI interrupt is actually delivered'
# build/usbstick.img is 16 MB = 32768 sectors of 512 bytes. READ CAPACITY
# returning the true geometry is what separates a driver that parsed the
# reply from one that assumed a size.
Expect 'usb-storage: 32768 sectors of 512 bytes' 'READ CAPACITY returned the true size'
# And then an actual transfer: block 1 of a v1 image is its superblock.
Expect 'block 1 magic 10203040' 'a real block came back off the USB disk'
Expect 'size 4096' 'the superblock read over USB is the right one'
}
if ($Sock) {
Expect 'socktest: ALL CHECKS PASSED' 'every socket check passed'
}
if ($Ps) {
# /proc is synthetic, so a per-process directory is the thing to prove.
# `ls /proc` prints in columns, so pid 1 is a token on a line rather than a
# line of its own.
Expect '(?m)^1\s+\d' '/proc has a directory for pid 1'
Expect '(?m)^1 \(' '/proc/1/stat starts with the pid and comm'
Expect 'sinit|init' 'ps names pid 1'
Expect '(?m)PID.*PPID.*COMM' 'ps honours -o with the requested columns'
Expect 'load average|Mem|CPU' 'top produces a report rather than an error'
}
if ($Evdev) {
# A key has to be *pressed* for this to say anything: characters typed at
# the serial port never reach the keyboard controller, which is why this
# run drives the QEMU monitor's sendkey instead.
Expect 'inputtest: done|event limit reached' 'the input layer delivered events'
Expect 'value' 'events carry a value, not just a type'
}
if ($script:fail) { exit 1 }
Write-Host 'boot check passed' -ForegroundColor Green