Three bugs, all of which surfaced as a failed check somewhere other than
the command that was actually slow.
Wait-Prompt waited a fixed 20 s. These tests run under TCG, not KVM, and
a cold `ls /bin | wc -l` takes 23 s on the Fedora laptop measured with
the guest's own /proc/uptime -- it was over the wall every time. Raised
to 60 s; a long timeout only costs time when something is genuinely
wrong, a short one costs a false failure elsewhere.
It also matched a prompt anywhere in the transcript. Between writing a
command and the guest echoing it the log still ends with the previous
prompt, so it could return before the command had started and the next
one was typed on top of it. Anchor the match to text produced after the
command was sent.
On timeout it slept two seconds and typed regardless, which is what
turned one slow command into an interleaved transcript and a failure
attributed to a later check. Wait for output to stop arriving instead.
Report any command that never produced a prompt next to the result:
after one overrun every later check is reading a transcript that may be
interleaved, and that should not be something you have to notice in the
scrollback.
With this, -Fb -Tty -Usb -Sock -Evdev -Ps is 108 [ok], 0 misses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `finally` stops QEMU on every path the script controls, but not when
the script itself is killed -- a CI timeout, Ctrl-C at the wrong moment,
pkill. QEMU then keeps running with no parent. Because these tests run
under TCG rather than KVM, a spare emulator pinning a core slows every
later run, and one was found still executing the guest at 86% of a core
eight minutes after its parent was gone.
That is the bad kind of failure: the suite does not break, it gets
slower, and commands drift past Wait-Prompt's timeout until the next one
is typed into a busy shell and the transcript interleaves. The check
that then fails is unrelated to whatever actually changed.
Record the pid on startup and reap the previous run's leftover before
starting a new one, the same self-healing approach already used for the
monitor port. The name is checked before killing, because pids are
recycled and killing an unrelated process would be worse than the leak.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither is Fedora-specific. A different host and a freshly built
userland simply drove code that Windows never reached.
tools/mkfs.c did not compile at all. The DT_UNKNOWN/DT_DIR/DT_REG
fallbacks sat above <dirent.h>, so on glibc -- which spells them as
enumerators -- the #ifndef asked before the header was read, always
answered "missing", and then the header's own enum expanded to
`enum { 0 = 0, ... }`. Move the fallbacks below the include. The
include cannot move up instead: it has to follow the #undef that ends
the `#define dirent schfs_dirent` block.
getstackpcs() page-faulted. It dereferenced rbp[1] without validating
rbp, and the guard rejected 0xffffffff -- dead code on x86-64, already
caught by the KERNBASE test -- while the value that actually turns up
is all-ones, which is above KERNBASE. rbp[1] then wrapped to 0x7 and
took trap 14. `cp` panicked the kernel every time. Bound rbp from
above and require 8-byte alignment.
That second one masks other bugs: getcallerpcs() runs on every lock
acquisition and again from the trap handler, so a walker that can
fault replaces a real diagnosis with a bogus one.
Baseline on the Fedora laptop is now green: make ports, make,
make schfs-check including the round-trip that had never run, and
bootcheck at 27/27 plus 108 [ok] across the extended suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/posix/setup-fedora.sh installs the toolchain and reports what it
cannot: PowerShell has no Fedora package, and bootcheck is the test
suite, so the script says how to get pwsh rather than failing quietly.
It also checks /dev/kvm, since a Ryzen box can run the tests at native
speed.
docs/moving-to-linux.md is the actual procedure: copy the repo, restore
the memory, install, build. The memory travels inside the repo and
`memory-sync.sh import` puts it under the key this machine derives from
its own path -- /home/nexoniarz/Documents/SchOS becomes
-home-nexoniarz-Documents-SchOS, where D:\DEV\SchOS was D--DEV-SchOS.
OVMF discovery no longer assumes QEMU's Windows layout. Fedora ships
edk2-ovmf to /usr/share/edk2/ovmf/, Debian and Arch differ again, so the
Makefile searches a list and names the packages when it finds nothing.
Tested through the shell rather than $(wildcard): on Windows make is a
native binary using Win32 file calls, while `command -v` returns an MSYS
path like /c/Users/..., which $(wildcard) cannot see -- it returned
nothing on a machine that had the firmware.
Two things only that machine can test: `make ports` builds musl, toybox
and mksh from source, which Windows cannot do at all; and the SchFS
round-trip needs symlinks and hard links, so it has never actually run.
Milestone 1 of three: manifests, RunAtLoad, KeepAlive with throttling,
and reaping. Replaces the hardcoded fork-a-shell loop in cmd/init, which
stays in the tree for now.
Jobs are sectioned key=value files in /etc/schinit/*.job, kept in the
repo under etc/ and installed into the sysroot -- sysroot/ is generated
and gitignored, so anything written only there is lost on a fresh clone,
which is already true of /etc/passwd and should not become true of the
boot configuration. Unknown keys and sections are ignored rather than
fatal: a manifest written for a newer schinit must not stop this one
booting the machine.
Socket sections are parsed and recorded but not yet held. That is
milestone 2 and it is the whole reason for copying launchd rather than
sysvinit -- the supervisor holds the listener and starts the job on first
connect, which inverts the dependency graph and makes runlevels a
non-concept. The key exists now so manifests need not change later.
getdents64 is exposed to userland for this. The kernel has had it since
the Linux ABI landed, but nothing declared it in user.h or lib/usys.S, so
first-party programs had no way to list a directory at all.
Two things found by testing rather than reasoning:
- setsid() gives a job its own session, and a session has no
controlling terminal. The console then delivers input only to the
foreground process group it tracks, so the shell came up, echoed
every keystroke through the line discipline, and executed none of
them -- indistinguishable from a hung shell. ControllingTerminal=yes
claims it with TIOCSCTTY/TIOCSPGRP; off by default, since a daemon
has no business seizing the console.
- the first bootstrap() dropped /dev/input/event0-1 and most of /proc,
because it was transcribed from the part of the old init that had
been read rather than all of it. -Evdev caught it.
build/usbstick.img is built by `all` now: without it QEMU refuses the
-drive and the entire -Usb run produces no output, which reads as a
broken USB driver.
Seven suites green: base 27, Tty 51, Ps 32, Sock 64, Fb 36, Usb 31,
Evdev 29. make schfs-check passes.
boot/bios/ now holds the MBR, the El Torito stub and the real-mode stage
2; boot/uefi/ is empty and documented.
Separate directories rather than #ifdefs, because the two share almost
nothing. A BIOS loader lives in 16-bit real mode, reaches the firmware
through software interrupts, and walks itself up into long mode. A UEFI
loader is a 64-bit PE that the firmware has already placed in long mode
with paging on, calling services through a table of function pointers.
What they do share is the hand-off contract, which boot/README.md now
writes down: ELF segments placed, long mode, a 24-byte-entry memory map
at E820_MAP_ADDR, framebuffer geometry at VBE_INFO_ADDR.
That contract is the point of documenting it now. UEFI's GetMemoryMap
returns EFI_MEMORY_DESCRIPTORs, and the right move is to translate them
into the entries kernel/arch/x86_64/e820.c already parses rather than
teach the kernel a second format.
Build output paths under $(BUILD)/boot are unchanged; only sources moved.
Two bugs reported from actual use, plus a test harness that had stopped
testing.
init could be killed by Ctrl-C, taking the machine with it. SIGINT goes
to the foreground process group, init is pgid 1, and any window where
tty_fg_pgrp is 1 -- before a shell claims the terminal, after one exits,
or between the two while the key is held -- lands it on init. init has
no handler, so killed was set, exit() ran, and exit() panics outright on
initproc. Spamming Ctrl-C at a frozen program is exactly the input that
finds that window. POSIX and Linux both discard a default-action signal
for pid 1; init_ignores() now does the same, SIGKILL included, because
init is not killable from userspace on any Unix.
Everything was slow because BSIZE was 1024 while a SchFS block is 4096,
so every block cost four IDE requests, each with its own interrupt and
sleep/wakeup. exec() of the 800 KB toybox binary meant ~800 round trips.
The giveaway: `seq 1 200`, which touches no file and prints nothing,
cost the same 2.0s as `cat /bin/toybox`, and `ls | wc -l` cost exactly
double -- both dominated by loading the same binary twice.
BSIZE is 4096 now, one request per SchFS block, and NBUF is 256 blocks
(1 MB) rather than 90 KB -- smaller than the binary every command execs,
so nothing was ever still resident. ideinit() issues SET MULTIPLE MODE,
which is what makes a 4096-byte transfer legal: READ/WRITE MULTIPLE move
the drive's multiple count per interrupt and nothing had ever set it,
which is the real reason idestart() carried a sector_per_block > 7 panic.
seq 1 200 2.02s -> 0.06s
ls /bin 2.76s -> 0.68s
ls /bin | wc -l 4.68s -> 0.73s
And the harness. bootcheck called Find-Qemu from a param() default,
which is bound before any function in the body exists -- so it died on
its first line and printed an error containing neither "FAIL" nor
"Exception", the two things being grepped for. It had not run since it
was made host-portable. Worse, an earlier index-based edit had deleted
the Expect blocks for -Tty, -Usb, -Sock, -Ps and -Evdev, so five of the
seven modes asserted nothing at all. Restored, and the USB run gets its
own 16 MB build/usbstick.img: it had been pointed at build/fs.img, which
nothing builds since SchFS became root, so QEMU refused to start.
242 checks across seven modes, all passing, and make schfs-check green.
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.
Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:
- D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
invisible until moved across by hand.
- Development happens from Windows and from Linux, and each looks in a
different place for the same checkout.
Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.
The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
Developed from both Windows and Linux, so nothing in the build should
care which host it runs on. It did, in four places.
Toolchain: one path instead of two. clang is a cross-compiler by
construction, so a single LLVM install targets bare-metal ELF, 16/32-bit
code below 1 MB, and Linux/musl userland from any host -- which is how
the Windows build already worked. The GNU branch beside it had drifted
into passing --no-warn-rwx-segments, a hard error on lld, which is the
default linker on both FreeBSD and macOS; and it required a brewed
x86_64-elf-gcc before anything built on a Mac at all. GNU is kept as a
fallback (USE_GNU=1) but clang+lld is chosen whenever both are present.
Host tools use the host's own cc; only mingw needs naming explicitly.
tools/mkschfs.c included <sys/sysmacros.h> unconditionally on non-mingw
hosts. That header is glibc/musl only -- macOS and the BSDs have neither
the header nor a need for it, since major()/minor() come from
<sys/types.h> there. Now guarded on __linux__.
Ports verified tarballs with `sha256sum -c -`, which does not exist on
macOS (shasum), FreeBSD/NetBSD (sha256), or OpenBSD (cksum -a sha256),
and each spells its check mode differently. tools/sha256check.sh picks
whichever of the five the host has and compares digests itself.
nproc is coreutils, so absent on macOS and BSD; falls through to sysctl
-n hw.ncpu before defaulting to 4.
The PowerShell tools now run under pwsh on any OS: QEMU is found on PATH
(the scoop location is a Windows-only fallback), and path literals use
forward slashes -- Join-Path $root 'build\x' produces a *filename
containing a backslash* on a Unix host. The backslash sweep is limited
to known directory prefixes on purpose: bootcheck sends
printf 'caf\303\251' to the guest to test UTF-8, and a blanket
substitution would have quietly changed what that asserts.
Also fixes six tools left booting build/fs.img, which stopped being
bootable when SchFS became the root filesystem: crashshot, dispshot,
fbshot, splashshot, textshot and ttyshot all take build/schfs.img now,
and the -SchFS switches that used to select it are gone.
Verified on Windows: full build, and bootcheck green. The Linux, macOS
and BSD paths are written from their documented behaviour and are NOT
executed here -- this machine has no WSL and no second host.
Removes the "-unix" element and with it any trademark exposure to The
Open Group.
The split follows the ecosystem layout: SchOS is the system, and the
kernel has its own name. Kernel-identity strings are the only places
that take SchIFT -- uname(2) sysname, /proc/version, and the two licence
sentences that say "kernel" outright. That is what every Unix does here:
`uname -s` on Linux reports the kernel, not the distribution. Everything
a user reads as the product -- panic screen, boot menu, error prefixes,
docs, LICENSE, NOTICE -- is SchOS.
schunix.img is schos.img, the logo assets and the musl TLS patch are
renamed, and __SCHUNIX__ is __SCHOS__ (the define and the patch that
consumes it moved together).
Two things the rename broke, both found by the bootcheck suite:
- shmtest wrote "SCHUNIX-MMAP-OK" with a hardcoded length of 15 and
compared it back by hand-counted offsets (m[3]=='U', m[14]=='K').
The shorter marker left the write running two bytes past the literal
and the compare reading past the data, failing a file-backed mmap
that was working correctly. Now written and compared through one
constant with sizeof.
- bootcheck asserted a 31-byte file after appending to
"hello-from-schunix"; the shorter string makes it 29.
Both are the same mistake: a string literal whose length is written out
somewhere else. Full suite green -- base, Tty, Ps, Sock, Fb, Usb, Evdev.
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.
No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.