tools: group scripts by what runs them
tools/windows/ .ps1 (pwsh, so usable from Linux and macOS too)
tools/posix/ .sh
tools/python/ .py
tools/ .c and .pl
The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.
Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.
Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:
- schfs_fsck.c did not compile under mingw at all: --extract uses
POSIX mkdir(path,mode), symlink() and link(), none of which mingw
has. That took the whole tool with it, and `make schfs-check` with
it. mkdir maps to _mkdir; symlink and link fail loudly rather than
writing a regular file in their place, because an extracted tree
that differs from the image while reporting success defeats the
point of --extract.
- the round-trip test then failed at `ln -s`, since Git Bash cannot
create symlinks without developer mode. It skips with a message now.
Not a fudge: mkschfs's mingw build cannot see a symlink either, so
the test would have compared a tree with none against an image with
none and called it a pass -- coverage reporting itself as present
while being absent.
Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.
This commit is contained in:
parent
531ef120ef
commit
754d96ebc5
26 changed files with 127 additions and 39 deletions
461
tools/windows/bootcheck.ps1
Normal file
461
tools/windows/bootcheck.ps1
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
# 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 = (Find-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>."
|
||||
}
|
||||
|
||||
$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'
|
||||
|
||||
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'
|
||||
# Always the v1 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/fs.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'
|
||||
|
||||
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/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 ($script:fail) { exit 1 }
|
||||
Write-Host 'boot check passed' -ForegroundColor Green
|
||||
141
tools/windows/crashshot.ps1
Normal file
141
tools/windows/crashshot.ps1
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# Boot SchOS, crash it on purpose, and capture the crash screen as an image.
|
||||
#
|
||||
# The crash screen is the one part of the system you cannot look at by using
|
||||
# the system, so it is the one part that quietly rots. This drives it through
|
||||
# /proc/sysrq and takes a picture, which turns "does the panic screen still
|
||||
# render" into something answerable in thirty seconds.
|
||||
#
|
||||
# QEMU's monitor is on its own socket so the serial console stays free for the
|
||||
# shell. screendump writes a PPM; the PNG conversion is optional and skipped if
|
||||
# ImageMagick is not around.
|
||||
#
|
||||
# tools/crashshot.ps1 crash the ramdisk-root build
|
||||
|
||||
param(
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Out = "build/crashscreen.ppm",
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$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: the kernel mounts
|
||||
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
|
||||
# not a choice and there is no -SchFS switch any more. It used to default to
|
||||
# build/fs.img, which stopped being bootable the moment SchFS became root.
|
||||
$hdb = Join-Path $root 'build/schfs.img'
|
||||
$log = Join-Path $root 'build/crashshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$mon = Join-Path $root 'build/qemu-mon.sock'
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
# The monitor goes to a TCP port rather than a unix socket: PowerShell can open
|
||||
# a TCP client without any extra machinery, and QEMU on Windows has no unix
|
||||
# sockets anyway.
|
||||
$monPort = 55555
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
|
||||
# Put something on the display first. The whole design of the crash screen
|
||||
# is that it dims what was there rather than clearing it, so a blank screen
|
||||
# underneath would prove nothing.
|
||||
Write-Host " > filling the screen with normal output" -ForegroundColor DarkGray
|
||||
Send-Line 'ls /bin'
|
||||
Start-Sleep -Seconds 3
|
||||
Send-Line 'cat /proc/cpuinfo'
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host " > echo c > /proc/sysrq" -ForegroundColor DarkGray
|
||||
Send-Line 'echo c > /proc/sysrq'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$stream = $client.GetStream()
|
||||
$writer = New-Object System.IO.StreamWriter($stream)
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
# QEMU on Windows wants a native path here, not the one PowerShell shows.
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
Write-Host "==> serial transcript" -ForegroundColor Cyan
|
||||
# Only the crash report, not the whole boot.
|
||||
$idx = $text.IndexOf('SchOS stopped')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) } else { Write-Host $text }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
$size = (Get-Item $ppm).Length
|
||||
Write-Host "==> $ppm ($size bytes)" -ForegroundColor Green
|
||||
$png = [System.IO.Path]::ChangeExtension($ppm, '.png')
|
||||
$magick = Get-Command magick -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& magick $ppm $png
|
||||
Write-Host "==> $png" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "FAIL: no screendump was produced" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
142
tools/windows/dispshot.ps1
Normal file
142
tools/windows/dispshot.ps1
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Boot SchOS, run the display self-test, and capture the result as an image.
|
||||
#
|
||||
# The display class cannot be checked without a display: the assertion is "did
|
||||
# the right pixels reach the screen", and the only instrument for that is a
|
||||
# screendump. This drives it through `echo d > /proc/sysrq` and takes a picture.
|
||||
#
|
||||
# What the pattern proves: four solid quadrants in theme colours (a stride error
|
||||
# would slant the bands, a bpp error would change the colours), a white frame one
|
||||
# pixel inside the edge (an off-by-one in the present rectangle would clip it),
|
||||
# and the fact that every pixel was drawn into RAM before a single byte reached
|
||||
# the framebuffer -- which is what double buffering means.
|
||||
#
|
||||
# QEMU's monitor is on its own socket so the serial console stays free for the
|
||||
# shell. screendump writes a PPM; the PNG conversion is optional and skipped if
|
||||
# ImageMagick is not around.
|
||||
#
|
||||
# tools/crashshot.ps1 crash the ramdisk-root build
|
||||
|
||||
param(
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Out = "build/dispshot.ppm",
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$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: the kernel mounts
|
||||
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
|
||||
# not a choice and there is no -SchFS switch any more. It used to default to
|
||||
# build/fs.img, which stopped being bootable the moment SchFS became root.
|
||||
$hdb = Join-Path $root 'build/schfs.img'
|
||||
$log = Join-Path $root 'build/crashshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$mon = Join-Path $root 'build/qemu-mon.sock'
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
# The monitor goes to a TCP port rather than a unix socket: PowerShell can open
|
||||
# a TCP client without any extra machinery, and QEMU on Windows has no unix
|
||||
# sockets anyway.
|
||||
$monPort = 55555
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
|
||||
# Show the device list first, so the transcript records what was bound.
|
||||
Write-Host " > listing devices" -ForegroundColor DarkGray
|
||||
Send-Line 'cat /proc/devices'
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
Write-Host " > echo d > /proc/sysrq" -ForegroundColor DarkGray
|
||||
Send-Line 'echo d > /proc/sysrq'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$stream = $client.GetStream()
|
||||
$writer = New-Object System.IO.StreamWriter($stream)
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
# QEMU on Windows wants a native path here, not the one PowerShell shows.
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
Write-Host "==> serial transcript" -ForegroundColor Cyan
|
||||
# Only the crash report, not the whole boot.
|
||||
$idx = $text.IndexOf('display: presented')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) } else { Write-Host $text }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
$size = (Get-Item $ppm).Length
|
||||
Write-Host "==> $ppm ($size bytes)" -ForegroundColor Green
|
||||
$png = [System.IO.Path]::ChangeExtension($ppm, '.png')
|
||||
$magick = Get-Command magick -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& magick $ppm $png
|
||||
Write-Host "==> $png" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "FAIL: no screendump was produced" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
115
tools/windows/fbshot.ps1
Normal file
115
tools/windows/fbshot.ps1
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Draw on /dev/fb0 and photograph the result.
|
||||
#
|
||||
# bootcheck -Fb proves fbinfo *ran* and that the numbers it read back are the
|
||||
# ones the bootloader programmed. It cannot prove the pixels landed where they
|
||||
# were meant to: a framebuffer written with the wrong stride still reports
|
||||
# every byte accepted, and the only symptom is a picture that shears
|
||||
# progressively down the screen. That is a thing you have to look at.
|
||||
#
|
||||
# tools/fbshot.ps1 gradient through mmap
|
||||
# tools/fbshot.ps1 -Mode bars colour bars through write()
|
||||
|
||||
param(
|
||||
[ValidateSet('fill', 'bars')]
|
||||
[string]$Mode = 'fill',
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$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: the kernel mounts
|
||||
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
|
||||
# not a choice and there is no -SchFS switch any more. It used to default to
|
||||
# build/fs.img, which stopped being bootable the moment SchFS became root.
|
||||
$hdb = Join-Path $root 'build/schfs.img'
|
||||
$log = Join-Path $root 'build/fbshot.log'
|
||||
$ppm = Join-Path $root "build/fb-$Mode.ppm"
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
$monPort = 55556 # not crashshot's 55555, so the two can run back to back
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
Write-Host " > fbinfo $Mode" -ForegroundColor DarkGray
|
||||
Send-Line "fbinfo $Mode"
|
||||
Start-Sleep -Seconds 8 # 1.9 MB a pixel at a time is not fast
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$writer = New-Object System.IO.StreamWriter($client.GetStream())
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
$idx = $text.IndexOf('fbinfo')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
Write-Host "==> $ppm ($((Get-Item $ppm).Length) bytes)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host 'FAIL: no screendump was produced' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
201
tools/windows/loc.ps1
Normal file
201
tools/windows/loc.ps1
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# How much of SchOS is actually SchOS.
|
||||
#
|
||||
# `wc -l` over the whole tree answers a question nobody asked: it counts musl,
|
||||
# toybox, mksh and a NetBSD checkout, which between them are
|
||||
# two orders of magnitude larger than this project and none of them ours. This
|
||||
# counts only first-party code, and separates the code from the comments so the
|
||||
# number means something.
|
||||
#
|
||||
# tools/loc.ps1 the summary table
|
||||
# tools/loc.ps1 -PerFile every file, largest first
|
||||
# tools/loc.ps1 -All include third-party trees, for comparison
|
||||
#
|
||||
# Comment detection is deliberately simple: it handles // and /* */ for C and
|
||||
# # for scripts, and does not try to understand a // inside a string literal.
|
||||
# That is worth being honest about -- the comment ratio is indicative, not
|
||||
# exact. The line and byte counts are exact.
|
||||
|
||||
param(
|
||||
[switch]$PerFile,
|
||||
[switch]$All,
|
||||
[int]$Top = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Repo root is two levels up: this script lives in tools/windows/.
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
|
||||
# What counts as ours. Everything else in the tree is either somebody else's
|
||||
# source or generated output, and mixing either in makes the total meaningless.
|
||||
$areas = [ordered]@{
|
||||
'kernel' = 'kernel'
|
||||
'include' = 'include'
|
||||
'boot' = 'boot'
|
||||
'lib' = 'lib'
|
||||
'cmd' = 'cmd'
|
||||
'tools' = 'tools'
|
||||
'docs' = 'docs'
|
||||
'projects' = 'projects'
|
||||
}
|
||||
if ($All) {
|
||||
$areas['ports (3rd party)'] = 'ports'
|
||||
$areas['src-trunk (NetBSD)'] = 'src-trunk'
|
||||
}
|
||||
|
||||
# Source only. Images, fonts-as-data and upstream tarballs are not code.
|
||||
$exts = @('.c', '.h', '.S', '.s', '.ld', '.inc', '.ps1', '.sh', '.pl', '.py', '.md', '.mk')
|
||||
|
||||
function Measure-Area([string]$rel) {
|
||||
$dir = Join-Path $root $rel
|
||||
$res = [PSCustomObject]@{
|
||||
Area = $rel; Files = 0; Lines = 0; Code = 0; Comment = 0; Blank = 0
|
||||
Bytes = 0; NonWs = 0; Alpha = 0; Digit = 0; Files_ = @()
|
||||
}
|
||||
if (-not (Test-Path $dir)) { return $res }
|
||||
|
||||
Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$f = $_
|
||||
if ($exts -notcontains $f.Extension.ToLower()) { return }
|
||||
# Makefiles have no extension; picked up separately below.
|
||||
$text = [System.IO.File]::ReadAllText($f.FullName)
|
||||
$lines = $text -split "`r?`n"
|
||||
# A trailing newline produces one empty element; do not count it.
|
||||
if ($lines.Length -gt 0 -and $lines[-1] -eq '') { $lines = $lines[0..($lines.Length-2)] }
|
||||
|
||||
$code = 0; $comment = 0; $blank = 0
|
||||
$inBlock = $false
|
||||
$isC = @('.c','.h','.s','.ld','.inc') -contains $f.Extension.ToLower()
|
||||
foreach ($l in $lines) {
|
||||
$t = $l.Trim()
|
||||
if ($t -eq '') { $blank++; continue }
|
||||
if ($inBlock) {
|
||||
$comment++
|
||||
if ($t -match '\*/') { $inBlock = $false }
|
||||
continue
|
||||
}
|
||||
if ($isC) {
|
||||
if ($t.StartsWith('//')) { $comment++; continue }
|
||||
if ($t.StartsWith('/*')) {
|
||||
$comment++
|
||||
if ($t -notmatch '\*/') { $inBlock = $true }
|
||||
continue
|
||||
}
|
||||
} elseif ($t.StartsWith('#') -and $f.Extension -ne '.md') {
|
||||
$comment++; continue
|
||||
}
|
||||
$code++
|
||||
}
|
||||
|
||||
$nonWs = ($text.ToCharArray() | Where-Object { -not [char]::IsWhiteSpace($_) }).Count
|
||||
$alpha = ($text.ToCharArray() | Where-Object { [char]::IsLetter($_) }).Count
|
||||
$digit = ($text.ToCharArray() | Where-Object { [char]::IsDigit($_) }).Count
|
||||
|
||||
$res.Files++
|
||||
$res.Lines += $lines.Length
|
||||
$res.Code += $code
|
||||
$res.Comment += $comment
|
||||
$res.Blank += $blank
|
||||
$res.Bytes += $f.Length
|
||||
$res.NonWs += $nonWs
|
||||
$res.Alpha += $alpha
|
||||
$res.Digit += $digit
|
||||
$res.Files_ += [PSCustomObject]@{
|
||||
Path = $f.FullName.Substring($root.Length + 1)
|
||||
Lines = $lines.Length; Code = $code; Comment = $comment
|
||||
}
|
||||
}
|
||||
return $res
|
||||
}
|
||||
|
||||
$results = @()
|
||||
foreach ($k in $areas.Keys) { $results += (Measure-Area $areas[$k]) }
|
||||
|
||||
# The top-level Makefile has no extension, so it is added by hand rather than
|
||||
# widening the extension list and dragging in every stray file in the tree.
|
||||
$mk = Join-Path $root 'Makefile'
|
||||
if (Test-Path $mk) {
|
||||
$mkLines = ([System.IO.File]::ReadAllText($mk) -split "`r?`n")
|
||||
$r = $results | Where-Object { $_.Area -eq 'tools' } | Select-Object -First 1
|
||||
if ($r) {
|
||||
$r.Files++; $r.Lines += $mkLines.Length; $r.Bytes += (Get-Item $mk).Length
|
||||
$r.Code += ($mkLines | Where-Object { $_.Trim() -ne '' -and -not $_.Trim().StartsWith('#') }).Count
|
||||
$r.Comment += ($mkLines | Where-Object { $_.Trim().StartsWith('#') }).Count
|
||||
$r.Blank += ($mkLines | Where-Object { $_.Trim() -eq '' }).Count
|
||||
}
|
||||
}
|
||||
|
||||
$fmt = @(
|
||||
@{ N='Area'; E={$_.Area} }
|
||||
@{ N='Files'; E={$_.Files}; A='right' }
|
||||
@{ N='Lines'; E={'{0:N0}' -f $_.Lines}; A='right' }
|
||||
@{ N='Code'; E={'{0:N0}' -f $_.Code}; A='right' }
|
||||
@{ N='Comment'; E={'{0:N0}' -f $_.Comment}; A='right' }
|
||||
@{ N='Blank'; E={'{0:N0}' -f $_.Blank}; A='right' }
|
||||
@{ N='Cmt%'; E={ if ($_.Code + $_.Comment) { '{0:N0}%' -f (100 * $_.Comment / ($_.Code + $_.Comment)) } else { '-' } }; A='right' }
|
||||
@{ N='Ln/File'; E={ if ($_.Files) { '{0:N0}' -f ($_.Lines / $_.Files) } else { '-' } }; A='right' }
|
||||
@{ N='KiB'; E={'{0:N0}' -f ($_.Bytes / 1KB)}; A='right' }
|
||||
)
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' SchOS — first-party source' -ForegroundColor Cyan
|
||||
Write-Host ''
|
||||
Write-Host (($results | Where-Object { $_.Files -gt 0 } | Sort-Object Lines -Descending | Format-Table $fmt -AutoSize | Out-String))
|
||||
|
||||
# Two totals, because they answer different questions and conflating them
|
||||
# flatters the number. "The OS" is what boots and runs on the machine; docs,
|
||||
# host-side tools and the projects/ stubs are first-party but are not SchOS
|
||||
# running on hardware. Linux 0.01's 10,239 lines were kernel, headers and boot
|
||||
# -- no documentation and no build tooling -- so only the first total is a fair
|
||||
# comparison.
|
||||
$osAreas = @('kernel','include','boot','lib','cmd')
|
||||
$own = $results | Where-Object { $_.Area -notin @('ports','src-trunk') }
|
||||
$os = $results | Where-Object { $_.Area -in $osAreas }
|
||||
$T = ($own | Measure-Object -Property Lines -Sum).Sum
|
||||
$C = ($own | Measure-Object -Property Code -Sum).Sum
|
||||
$M = ($own | Measure-Object -Property Comment -Sum).Sum
|
||||
$B = ($own | Measure-Object -Property Blank -Sum).Sum
|
||||
$F = ($own | Measure-Object -Property Files -Sum).Sum
|
||||
$By = ($own | Measure-Object -Property Bytes -Sum).Sum
|
||||
$NW = ($own | Measure-Object -Property NonWs -Sum).Sum
|
||||
$AL = ($own | Measure-Object -Property Alpha -Sum).Sum
|
||||
$DG = ($own | Measure-Object -Property Digit -Sum).Sum
|
||||
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Files', $F)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Lines total', $T)
|
||||
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which code', $C, (100 * $C / [Math]::Max($T,1)))
|
||||
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which comment', $M, (100 * $M / [Math]::Max($T,1)))
|
||||
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which blank', $B, (100 * $B / [Math]::Max($T,1)))
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Lines per file', ($T / [Math]::Max($F,1)))
|
||||
Write-Host ''
|
||||
$osT = ($os | Measure-Object -Property Lines -Sum).Sum
|
||||
$osC = ($os | Measure-Object -Property Code -Sum).Sum
|
||||
$osF = ($os | Measure-Object -Property Files -Sum).Sum
|
||||
Write-Host (' {0,-22} {1,12:N0} ({2:N0} code, {3} files)' -f 'THE OS ITSELF', $osT, $osC, $osF) -ForegroundColor Green
|
||||
Write-Host (' {0,-22}' -f ' kernel+include+boot+lib+cmd') -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Bytes', $By)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Non-whitespace chars', $NW)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f ' letters', $AL)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f ' digits', $DG)
|
||||
Write-Host ''
|
||||
|
||||
# For scale. Linux 0.01 (September 1991) is 10,239 lines by the same crude
|
||||
# measure -- every line of every file in the tarball, comments included.
|
||||
$lin = 10239
|
||||
Write-Host (' For scale: Linux 0.01 was {0:N0} lines (kernel, headers, boot -- the' -f $lin) -ForegroundColor DarkGray
|
||||
Write-Host (' same things counted above as THE OS ITSELF). SchOS is {0:N1}x that.' -f ($osT / $lin)) -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
|
||||
if ($PerFile) {
|
||||
Write-Host ' Largest files' -ForegroundColor Cyan
|
||||
# Out-String, not a bare Format-Table: two Format-Table calls writing to
|
||||
# the same output stream interleave their grouping records and PowerShell
|
||||
# rejects the second one outright.
|
||||
$rows = ($own | ForEach-Object { $_.Files_ }) |
|
||||
Sort-Object Lines -Descending | Select-Object -First $Top |
|
||||
Format-Table @{N='Lines';E={'{0:N0}' -f $_.Lines};A='right'},
|
||||
@{N='Code'; E={'{0:N0}' -f $_.Code};A='right'},
|
||||
@{N='Cmt'; E={'{0:N0}' -f $_.Comment};A='right'},
|
||||
Path -AutoSize | Out-String
|
||||
Write-Host $rows
|
||||
}
|
||||
177
tools/windows/makeiso.ps1
Normal file
177
tools/windows/makeiso.ps1
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Build a bootable SchOS ISO, ready for VMware or a real machine.
|
||||
#
|
||||
# Wraps `make iso`, which builds the ramdisk-root variant and wraps it in an
|
||||
# El Torito image. The ramdisk root is the point: the ISO carries a complete
|
||||
# userland inside the kernel, so it boots with no hard disk attached at all.
|
||||
#
|
||||
# tools/makeiso.ps1 build it
|
||||
# tools/makeiso.ps1 -Run build it, then boot it under QEMU to check
|
||||
# tools/makeiso.ps1 -Clean force a full rebuild first
|
||||
#
|
||||
# Afterwards the ISO is schos.iso in the repository root; point a VM's optical
|
||||
# drive at it and boot from CD.
|
||||
|
||||
param(
|
||||
[switch]$Run,
|
||||
[switch]$Clean,
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Repo root is two levels up: this script lives in tools/windows/.
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
Push-Location $root
|
||||
|
||||
function Fail($msg) {
|
||||
Write-Host "FAIL: $msg" -ForegroundColor Red
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Git Bash, found by full path and never as bare `bash`.
|
||||
#
|
||||
# From PowerShell, `bash` resolves to C:\...\WindowsApps\bash.exe -- the *WSL*
|
||||
# launcher -- long before anything on PATH. On this machine WSL2 cannot start at
|
||||
# all, because it needs the Hyper-V hypervisor underneath the whole system and
|
||||
# that is deliberately off. So calling `bash` here does not run a different
|
||||
# shell, it fails with a wall of virtualisation advice and an exit code that
|
||||
# looks like the build broke.
|
||||
$BashCandidates = @(
|
||||
"$env:ProgramFiles\Git\bin\bash.exe"
|
||||
"${env:ProgramFiles(x86)}\Git\bin\bash.exe"
|
||||
"$env:USERPROFILE\scoop\apps\git\current\bin\bash.exe"
|
||||
"$env:LOCALAPPDATA\Programs\Git\bin\bash.exe"
|
||||
)
|
||||
$Bash = $BashCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $Bash) { Fail "Git Bash not found; looked in:`n $($BashCandidates -join "`n ")" }
|
||||
|
||||
function Invoke-Make([string]$cmd) {
|
||||
# -c, not -lc: a login shell sources the profile for no benefit here.
|
||||
# stderr folded in inside bash, because $ErrorActionPreference = 'Stop'
|
||||
# turns a native program's stderr into a terminating error, and mkisofs
|
||||
# writes a harmless warning there on every single run.
|
||||
& $Bash -c "cd '$($root -replace '\\','/')' && $cmd 2>&1"
|
||||
}
|
||||
|
||||
# An ISO needs one of these to assemble the filesystem. There is no xorriso
|
||||
# package for Windows in scoop; cdrtools provides mkisofs, and tools/mkiso.sh
|
||||
# accepts either -- their El Torito arguments are compatible.
|
||||
$haveIso = (Get-Command xorriso -ErrorAction SilentlyContinue) -or
|
||||
(Get-Command mkisofs -ErrorAction SilentlyContinue)
|
||||
if (-not $haveIso) {
|
||||
Write-Host "Need xorriso or mkisofs on PATH." -ForegroundColor Yellow
|
||||
Write-Host " Windows: scoop install cdrtools"
|
||||
Write-Host " Debian/Ubuntu: apt install xorriso"
|
||||
Fail "no ISO builder"
|
||||
}
|
||||
|
||||
if ($Clean) {
|
||||
Write-Host "==> cleaning" -ForegroundColor Cyan
|
||||
# Not `make clean`: that would drop the ports too, and those take an
|
||||
# afternoon to rebuild on a host where they are not rebuilt at all.
|
||||
Invoke-Make "rm -rf build/kernel build/lib build/cmd" | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "==> building (this makes the ramdisk-root kernel, which is larger)" -ForegroundColor Cyan
|
||||
|
||||
# Output captured whole rather than piped through a filter. Piping a native
|
||||
# command into ForEach-Object leaves $LASTEXITCODE reflecting the pipeline
|
||||
# rather than the command, which reported a perfectly successful `make iso` as
|
||||
# a failure. Redirect stderr inside bash too: with $ErrorActionPreference set to
|
||||
# Stop, PowerShell turns a native program's stderr into terminating errors, and
|
||||
# mkisofs writes a harmless warning there every single run.
|
||||
$out = Invoke-Make "make iso"
|
||||
$code = $LASTEXITCODE
|
||||
$out | Where-Object { $_ -match '^==>|mkiso:|nmeta|rror' } |
|
||||
ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||
if ($code -ne 0) {
|
||||
$out | Select-Object -Last 20 | ForEach-Object { Write-Host $_ }
|
||||
Fail "make iso returned $code"
|
||||
}
|
||||
|
||||
$iso = Join-Path $root 'schos.iso'
|
||||
if (-not (Test-Path $iso)) { Fail "schos.iso was not produced" }
|
||||
|
||||
# `make iso` leaves the tree holding objects compiled with -DRAMDISK_ROOT. A
|
||||
# following plain `make` would then fail to link, on a missing ramdisk symbol,
|
||||
# for reasons that look nothing like their cause. Put it back.
|
||||
Write-Host "==> restoring the normal (disk-root) build" -ForegroundColor Cyan
|
||||
Invoke-Make "rm -rf build/kernel build/lib build/cmd && make >/dev/null" | Out-Null
|
||||
|
||||
$mb = [Math]::Round((Get-Item $iso).Length / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-Host "==> $iso ($mb MB)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "VMware:" -ForegroundColor Cyan
|
||||
Write-Host " New VM -> I will install the OS later -> Other / Other 64-bit"
|
||||
Write-Host " CD/DVD -> Use ISO image file -> $iso, and tick Connect at power on"
|
||||
Write-Host " Boot order: CD-ROM first (F2 at power on for the firmware setup)"
|
||||
Write-Host ""
|
||||
Write-Host " Set the VM to BIOS, not UEFI. SchOS's loader is 16-bit real"
|
||||
Write-Host " mode and there is no EFI stub yet, so a UEFI-only VM will not boot."
|
||||
Write-Host ""
|
||||
Write-Host " Processors: as many as you like. SMP works as of 2026-08-08;"
|
||||
Write-Host " check with 'cat /proc/cpuinfo', which lists the online ones."
|
||||
Write-Host ""
|
||||
Write-Host " RAM: 64 MB is plenty. The old 224 MB floor is gone."
|
||||
|
||||
if ($Run) {
|
||||
Write-Host ""
|
||||
Write-Host "==> booting the ISO under QEMU, no disk attached" -ForegroundColor Cyan
|
||||
$log = Join-Path $root 'build/isoboot.log'
|
||||
if (Test-Path $log) { Clear-Content $log }
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-cdrom `"$iso`" -boot d"
|
||||
'-m 256M -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
'-no-reboot'
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
Start-Sleep -Seconds 25
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 600
|
||||
|
||||
$text = if (Test-Path $log) { Get-Content $log -Raw } else { "" }
|
||||
if ($text -match 'init: ready') {
|
||||
Write-Host "==> the ISO boots to a shell" -ForegroundColor Green
|
||||
($text -split "`n" | Select-Object -First 12) | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||
} else {
|
||||
Write-Host "==> the ISO did NOT reach a shell" -ForegroundColor Red
|
||||
Write-Host $text
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
162
tools/windows/splashshot.ps1
Normal file
162
tools/windows/splashshot.ps1
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Photograph the boot splash.
|
||||
#
|
||||
# The splash is on screen from the moment stage2 draws it until
|
||||
# console_takeover() clears it at the first exec(), which is a window of a
|
||||
# couple of seconds -- so a single screendump at a guessed delay is a coin
|
||||
# flip. This takes a burst instead and keeps every frame, which makes the
|
||||
# question "what does boot actually look like, second by second" answerable
|
||||
# rather than a matter of catching it at the right instant.
|
||||
#
|
||||
# Same shape as tools/crashshot.ps1: monitor on TCP because QEMU on Windows has
|
||||
# no unix sockets, serial to a logfile so the transcript survives.
|
||||
#
|
||||
# tools/splashshot.ps1 frames every 400ms for 8s
|
||||
# tools/splashshot.ps1 -Every 200 -For 4
|
||||
|
||||
param(
|
||||
[int]$Every = 400, # milliseconds between frames
|
||||
[int]$For = 8, # seconds to keep capturing
|
||||
[string]$OutDir = "build/shots",
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Repo root is two levels up: this script lives in tools/windows/.
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$log = Join-Path $root 'build/splashshot.log'
|
||||
$dir = Join-Path $root $OutDir
|
||||
|
||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $dir | Out-Null
|
||||
Remove-Item -Force $log -ErrorAction SilentlyContinue
|
||||
|
||||
$hda = Join-Path $root 'schos.img'
|
||||
# The root filesystem. SchFS is the only root there is: the kernel mounts
|
||||
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
|
||||
# not a choice and there is no -SchFS switch any more. It used to default to
|
||||
# build/fs.img, which stopped being bootable the moment SchFS became root.
|
||||
$hdb = Join-Path $root 'build/schfs.img'
|
||||
|
||||
$monPort = 55556 # not crashshot's port, so the two can run back to back
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
# -snapshot with file.locking=off, rather than booting from copies. QEMU
|
||||
# takes an exclusive lock on its drive images, so an interactive session the
|
||||
# developer already has open would otherwise make this fail with "Failed to
|
||||
# get write lock" -- and a capture script is exactly the thing you want to
|
||||
# run without first shutting down what you were looking at. Copying was the
|
||||
# first fix and it meant duplicating build/fs.img, all 153 MB of it, on
|
||||
# every run. -snapshot keeps writes in a temporary overlay, so the originals
|
||||
# are untouched either way.
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk,file.locking=off"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk,file.locking=off"
|
||||
'-snapshot'
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
$frames = @()
|
||||
try {
|
||||
# Connect immediately -- the interesting frames are the early ones, so any
|
||||
# fixed warm-up sleep here is time spent missing them.
|
||||
$client = $null
|
||||
for ($i = 0; $i -lt 40 -and -not $client; $i++) {
|
||||
try { $client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort) }
|
||||
catch { Start-Sleep -Milliseconds 100 }
|
||||
}
|
||||
if (-not $client) { throw "could not reach the QEMU monitor on port $monPort" }
|
||||
|
||||
$writer = New-Object System.IO.StreamWriter($client.GetStream())
|
||||
$writer.AutoFlush = $true
|
||||
|
||||
$n = [int](($For * 1000) / $Every)
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
for ($i = 0; $i -lt $n; $i++) {
|
||||
$ms = [int]$sw.ElapsedMilliseconds
|
||||
$f = Join-Path $dir ("{0:d5}ms.ppm" -f $ms)
|
||||
$writer.WriteLine("screendump $f")
|
||||
$frames += $f
|
||||
Start-Sleep -Milliseconds $Every
|
||||
}
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
# Report each frame by its distinct-colour count. Two colours means a text
|
||||
# console (or a blank screen); an anti-aliased logo pushes it into the dozens.
|
||||
# The same reason the full-stack harness counted pixels rather than log lines: a capture
|
||||
# that exists is not a capture that drew anything.
|
||||
Write-Host "==> frames" -ForegroundColor Cyan
|
||||
$kept = 0
|
||||
foreach ($f in $frames) {
|
||||
if (-not (Test-Path $f)) { continue }
|
||||
$kept++
|
||||
$b = [System.IO.File]::ReadAllBytes($f)
|
||||
# PPM P6: "P6\n<w> <h>\n255\n" then raw RGB. Skip three whitespace-run
|
||||
# separated header fields rather than assuming a fixed header length --
|
||||
# QEMU's is compact but that is not guaranteed.
|
||||
$pos = 2; $fields = 0
|
||||
while ($fields -lt 3 -and $pos -lt $b.Length) {
|
||||
while ($pos -lt $b.Length -and [char]$b[$pos] -match '\s') { $pos++ }
|
||||
while ($pos -lt $b.Length -and [char]$b[$pos] -notmatch '\s') { $pos++ }
|
||||
$fields++
|
||||
}
|
||||
$pos++
|
||||
$seen = @{}
|
||||
for ($i = $pos; $i + 2 -lt $b.Length; $i += 3 * 7) { # sample every 7th pixel
|
||||
$seen[($b[$i] -shl 16) -bor ($b[$i+1] -shl 8) -bor $b[$i+2]] = 1
|
||||
}
|
||||
$name = Split-Path $f -Leaf
|
||||
$c = $seen.Count
|
||||
$tag = if ($c -ge 16) { 'graphical' } elseif ($c -le 3) { 'flat/text' } else { 'sparse' }
|
||||
$col = if ($c -ge 16) { 'Green' } else { 'DarkGray' }
|
||||
Write-Host (" {0,-12} {1,4} colours {2}" -f $name, $c, $tag) -ForegroundColor $col
|
||||
}
|
||||
|
||||
if ($kept -eq 0) { Write-Host "FAIL: no frames captured" -ForegroundColor Red; exit 1 }
|
||||
Write-Host "==> $kept frames in $dir" -ForegroundColor Green
|
||||
|
||||
# Convert to PNG if ImageMagick is around, so they can just be looked at.
|
||||
if (Get-Command magick -ErrorAction SilentlyContinue) {
|
||||
foreach ($f in $frames) {
|
||||
if (Test-Path $f) { & magick $f ([System.IO.Path]::ChangeExtension($f, '.png')) }
|
||||
}
|
||||
Write-Host "==> PNGs alongside" -ForegroundColor Green
|
||||
}
|
||||
142
tools/windows/textshot.ps1
Normal file
142
tools/windows/textshot.ps1
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Boot SchOS, run the text-rendering self-test, and capture the result as an image.
|
||||
#
|
||||
# The display class cannot be checked without a display: the assertion is "did
|
||||
# the right pixels reach the screen", and the only instrument for that is a
|
||||
# screendump. This drives it through `echo t > /proc/sysrq` and takes a picture.
|
||||
#
|
||||
# What the pattern proves: four solid quadrants in theme colours (a stride error
|
||||
# would slant the bands, a bpp error would change the colours), a white frame one
|
||||
# pixel inside the edge (an off-by-one in the present rectangle would clip it),
|
||||
# and the fact that every pixel was drawn into RAM before a single byte reached
|
||||
# the framebuffer -- which is what double buffering means.
|
||||
#
|
||||
# QEMU's monitor is on its own socket so the serial console stays free for the
|
||||
# shell. screendump writes a PPM; the PNG conversion is optional and skipped if
|
||||
# ImageMagick is not around.
|
||||
#
|
||||
# tools/crashshot.ps1 crash the ramdisk-root build
|
||||
|
||||
param(
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Out = "build/textshot.ppm",
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$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: the kernel mounts
|
||||
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
|
||||
# not a choice and there is no -SchFS switch any more. It used to default to
|
||||
# build/fs.img, which stopped being bootable the moment SchFS became root.
|
||||
$hdb = Join-Path $root 'build/schfs.img'
|
||||
$log = Join-Path $root 'build/crashshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$mon = Join-Path $root 'build/qemu-mon.sock'
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
# The monitor goes to a TCP port rather than a unix socket: PowerShell can open
|
||||
# a TCP client without any extra machinery, and QEMU on Windows has no unix
|
||||
# sockets anyway.
|
||||
$monPort = 55555
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
|
||||
# Show the device list first, so the transcript records what was bound.
|
||||
Write-Host " > listing devices" -ForegroundColor DarkGray
|
||||
Send-Line 'cat /proc/devices'
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
Write-Host " > echo t > /proc/sysrq" -ForegroundColor DarkGray
|
||||
Send-Line 'echo t > /proc/sysrq'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$stream = $client.GetStream()
|
||||
$writer = New-Object System.IO.StreamWriter($stream)
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
# QEMU on Windows wants a native path here, not the one PowerShell shows.
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
Write-Host "==> serial transcript" -ForegroundColor Cyan
|
||||
# Only the crash report, not the whole boot.
|
||||
$idx = $text.IndexOf('display: presented')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) } else { Write-Host $text }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
$size = (Get-Item $ppm).Length
|
||||
Write-Host "==> $ppm ($size bytes)" -ForegroundColor Green
|
||||
$png = [System.IO.Path]::ChangeExtension($ppm, '.png')
|
||||
$magick = Get-Command magick -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& magick $ppm $png
|
||||
Write-Host "==> $png" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "FAIL: no screendump was produced" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
120
tools/windows/ttyshot.ps1
Normal file
120
tools/windows/ttyshot.ps1
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Photograph the framebuffer console after driving it with escape sequences.
|
||||
#
|
||||
# The serial line and the framebuffer are two different consumers of console
|
||||
# output, and they behave differently on purpose: escapes are passed through
|
||||
# to the serial line verbatim (a real terminal emulator is on the other end
|
||||
# and understands them better than we do) and *interpreted* for the
|
||||
# framebuffer. So the serial transcript can never tell you whether the escape
|
||||
# parser works -- it contains the raw bytes either way. Only a picture can.
|
||||
#
|
||||
# tools/ttyshot.ps1
|
||||
|
||||
param(
|
||||
[int]$BootSeconds = 22,
|
||||
[string]$Out = "build/ttyshot.ppm",
|
||||
[string]$Qemu = (Find-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>."
|
||||
}
|
||||
|
||||
$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: the kernel mounts
|
||||
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
|
||||
# not a choice and there is no -SchFS switch any more. It used to default to
|
||||
# build/fs.img, which stopped being bootable the moment SchFS became root.
|
||||
$hdb = Join-Path $root 'build/schfs.img'
|
||||
$log = Join-Path $root 'build/ttyshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$monPort = 55571
|
||||
|
||||
# Each line exercises one thing the console could not do before: colour,
|
||||
# bright colour, background colour, cursor addressing, erase-to-end-of-line,
|
||||
# and non-ASCII text.
|
||||
$commands = @(
|
||||
'clear'
|
||||
'printf ''\033[31mRED \033[32mGREEN \033[34mBLUE \033[35mMAGENTA \033[36mCYAN \033[0mdefault\n'''
|
||||
'printf ''\033[1;31mbold red \033[1;32mbold green \033[1;34mbold blue\033[0m\n'''
|
||||
'printf ''\033[37;44m white on blue \033[0m normal\n'''
|
||||
'printf ''caf\303\251 \303\251t\303\251 na\303\257ve \303\274ber sch\303\266n a\303\261o\n'''
|
||||
'printf ''ERASED-THIS\033[13G\033[Kkept this\n'''
|
||||
'printf ''\033[12;40Hcursor addressed to row 12 col 40\n'''
|
||||
# Tab used to be drawn as glyph 9, which in CP437 is a hollow circle --
|
||||
# so pressing Tab printed a circle instead of moving the cursor. Every
|
||||
# other control character had the same problem waiting behind it.
|
||||
'printf ''A\tB\tC\tD -- tabs, not circles\n'''
|
||||
)
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host '==> booting' -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
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 20 # serial has no flow control
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
Write-Host '==> capturing the framebuffer' -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$writer = New-Object System.IO.StreamWriter($client.GetStream())
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
Write-Host "==> $ppm ($((Get-Item $ppm).Length) bytes)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host 'FAIL: no screendump was produced' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Loading…
Reference in a new issue