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:
Nexoniarz 2026-08-09 14:38:02 +02:00
commit 754d96ebc5
26 changed files with 127 additions and 39 deletions

461
tools/windows/bootcheck.ps1 Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
}