RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

foundry / scripts / run-headless.sh
14488 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env bash
# Boot a Foundry OS image headless on the pinned Rustbelt kernel and check
# the self-test's output on the serial console. Two boot contracts
# (docs/boot-contract.md), picked with CONTRACT=v1|v2 (default v1):
#
#   v1: the kernel runs the shell self-test (B:/scripts/selftest.sh from
#       rootfs/) itself. Our image is disk 0, the kernel's three test disks
#       C:, D:, E: (snapshot: nothing is written back). The expected lines
#       are the kernel repo's own (moved from its scripts/run-headless.sh),
#       so a pass means "same behaviour as the kernel's own image".
#   v2: our /sbin/init runs /scripts/selftest.sh (from root/) and ends the
#       run with its exit status. Only our image is attached (A:, B:, C:
#       are all partitions of it), WITHOUT snapshot, so the caller can
#       check afterwards what was written (make test-v2 boots a copy).
#       The expected lines are all in EXTRA_EXPECT (scripts/expect-v2.txt).
#
# Usage: [CONTRACT=v2] scripts/run-headless.sh IMAGE KERNEL_BOOTIMAGE KERNEL_TARGET OUT_DIR
#   IMAGE             our disk image
#   KERNEL_BOOTIMAGE  out/kernel/bootimage-headless.bin
#   KERNEL_TARGET     out/kernel-target (has the kernel-owned test disks
#                     ext2.img, exfat.img, btrfs.img: drives C:, D:, E:)
#   OUT_DIR           where logs go (out/)
# Env (the Makefile sets these from config.mk; they change with the kernel pin):
#   KERNEL_BANNER   the kernel's first line, e.g. "Rustbelt 0.21.0 (stage 21: ...)"
#   KERNEL_VERSION  e.g. 0.21.0
#   BIN_COUNT       how many programs the image has in B:/bin (`dir bin | wc -l`)
#   ROOT_BIN_COUNT  v2: how many programs are in A:/bin (@ROOT_BIN_COUNT@)
#   EXTRA_EXPECT    optional file: more lines the log must contain, one per
#                   line (Foundry OS's own checks; # starts a comment)
#   QEMU_CPU (default max), QEMU_TIMEOUT (default 120 s).
#
# Only the QEMU this script starts is ever killed: QEMU writes its PID to
# OUT_DIR/qemu-headless.pid and the exit trap kills exactly that PID.
set -euo pipefail
cd "$(dirname "$0")/.."            # repo root: QEMU's guestfwd runs scripts/http-reply.sh
IMAGE=$1 IMG=$2 KT=$3 OUT=$4
CONTRACT=${CONTRACT:-v1}
: "${KERNEL_BANNER:?set KERNEL_BANNER (see config.mk)}" "${KERNEL_VERSION:?}" "${BIN_COUNT:?}"
STAGE=${KERNEL_BANNER#*(}; STAGE=${STAGE%%:*}     # "stage 20"
mkdir -p "$OUT"
if [ "$CONTRACT" = v2 ]; then
    DISKS=(-drive file="$IMAGE",format=raw,if=virtio)
else
    DISKS=(-drive file="$IMAGE",format=raw,if=virtio,snapshot=on)
    for d in ext2 exfat btrfs; do
        DISKS+=(-drive file="$KT/$d.img",format=raw,if=virtio,snapshot=on)
    done
fi
for f in "$IMAGE" "$IMG"; do
    [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; }
done
for d in "${DISKS[@]}"; do
    case $d in file=*) f=${d#file=}; f=${f%%,*}
        [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; } ;; esac
done

PIDFILE=$OUT/qemu-headless.pid
rm -f "$PIDFILE"
cleanup() {
    if [ -s "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
        kill "$(cat "$PIDFILE")" 2>/dev/null || true
    fi
    [ -n "${CURL_PID:-}" ] && kill "$CURL_PID" 2>/dev/null || true
}
trap cleanup EXIT

# Network: QEMU's user-mode NAT. guestfwd runs a tiny test web server for
# 10.0.2.100:80 (and `cat` as an echo server on :7); hostfwd forwards a free
# host port to the guest's port 80, where the self-test's `httpd -n 1`
# waits, and a background curl below fetches its page.
HTTP_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')
NETDEV="user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat"
NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80"
CURL_OUT=$OUT/headless-curl.txt
rm -f "$CURL_OUT"
( for _ in $(seq 1 80); do
      sleep 1
      curl -s -m 5 -o "$CURL_OUT" "http://127.0.0.1:$HTTP_PORT/" 2>/dev/null && [ -s "$CURL_OUT" ] && break
  done ) &
CURL_PID=$!

# virtio-gpu, virtio-rng, and a virtio console (hvc0) fed from
# scripts/hvc0-session.txt; its output goes to OUT/headless-hvc0.txt.
HVC_OUT=$OUT/headless-hvc0.txt
rm -f "$HVC_OUT"
VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci
    -device virtio-serial-pci -device virtconsole,chardev=hvc
    -chardev "file,id=hvc,path=$HVC_OUT,input-path=scripts/hvc0-session.txt")

LOG=$OUT/headless-serial.log
echo "[run-headless] kernel $IMG"
echo "[run-headless] contract $CONTRACT, disk 0 = $IMAGE"
set +e
timeout "${QEMU_TIMEOUT:-120}" qemu-system-x86_64 -smp 4 -cpu "${QEMU_CPU:-max}" \
    -pidfile "$PIDFILE" \
    -drive format=raw,file="$IMG",snapshot=on \
    "${DISKS[@]}" \
    -netdev "$NETDEV" -device virtio-net-pci,netdev=net0 \
    "${VIRTIO_DEVICES[@]}" \
    -device isa-debug-exit,iobase=0xf4,iosize=0x04 \
    -serial stdio -display none -no-reboot | tee "$LOG"
code=${PIPESTATUS[0]}
kill $CURL_PID 2>/dev/null; wait $CURL_PID 2>/dev/null
set -e

# Lines the shell self-test must have printed (same list as the kernel repo at the pinned commit).
if [ "$code" = 33 ] && [ "$CONTRACT" = v2 ]; then
    expect=()   # all in EXTRA_EXPECT
    # Pipe results: `dir bin | wc -l` on B: -> $BIN_COUNT, the todo.txt grep -> 1,
    # `cat C:/seq.txt | wc -l` (downloaded by wget) -> 20000.
    PIPE_RESULTS=("$BIN_COUNT" 1 20000)
    # init's own memory check: frames before the self-test == after it and
    # every process it started (orphans included) were gone.
    FRAMES_RE='^\[init\] memory: ([0-9]+) frames in use before the self-test, \1 after'
elif [ "$code" = 33 ]; then
    PIPE_RESULTS=(1 "$BIN_COUNT" 3 4 20000)
    FRAMES_RE='^\[memory\] ([0-9]+) frames in use before the shell, \1 after'
    expect=(
        "$KERNEL_BANNER x86_64"                       # ver
        "[smp] 4 CPUs online"                          # smp.rs started 3 more cores
        "[fpu] "                                       # XSAVE (-cpu max) or FXSAVE (qemu64)
        "B:  /mnt/disk0   fat32"                       # drives
        "C:  /mnt/disk1   ext2"
        "D:  /mnt/disk2   exfat"
        "E:  /mnt/disk3   btrfs"
        "B:/ $ ls"                                     # cd B: changed the prompt
        "readme.txt  scripts/"                         # ls in columns
        "In the Rustbelt shell this disk is drive B:"  # type B:/readme.txt
        "second line"                                  # >> and <
        "       125  todo.txt"                         # cp + ls -l
        '[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3'   # run --trace
        "sleep started in the background"              # start
        "[done] pid 19 (start sleep 1), exit code 0"   # wait
        "C:/ $ ls"                                     # the ext2 disk
        "Hello from the ext2 disk!"                    # cat through a symlink
        "big.txt line 12345"                           # double-indirect block
        "written by Rustbelt"                          # mkdir + create on ext2
        "-rw-r--r--        125  todo.txt"              # cp B: -> C:, ls -l
        "lrwxrwxrwx             hello-link -> hello.txt"
        "drwxr-xr-x      <DIR>  made-here/"
        "D:/ $ ls"                                     # the exFAT disk
        "This file has a long name with spaces in it." # quoted name, other case
        "MixedCase.TXT: exFAT keeps the case"          # cp D: -> C:, cat
        "hi from exFAT"                                # md + echo >, other case
        "       125  todo.txt"                         # cp B: -> D:, ls -l D:/
        "     <DIR>  New Folder/"
        "    340000  numbers.txt"
        "E:/ $ ls -l"                                  # the btrfs disk
        "lrwxrwxrwx             hello-link -> hello.txt"
        "Hello from btrfs! (an inline extent"          # inline, through a symlink
        "btrfs never overwrites a block in place"      # compressed inline extent
        " 120000  360000 2160000 big.txt"              # 17 zlib extents
        "btrfs line 119999"
        "Stage 10: a read-only btrfs driver"           # inside subvolume projects
        "sh: new.txt: Read-only file system"           # writes refused
        "rm: hello.txt: Read-only file system"
        "mkdir: newdir: Read-only file system"
        "[exit code 139]"                              # run evil
        "(overflow) killed: stack overflow"            # user stack guard zone
        "4 CPUs online (this program ran on CPU"       # sysinfo
        "  CPU3 ["
        "[net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3" # net/
        "      inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)" # ifconfig
        "64 bytes from 10.0.2.2: icmp_seq=3"           # ping the slirp gateway
        "3 packets transmitted, 3 received, 0% packet loss"
        "Hello from the host, via QEMU guestfwd!"      # fetch (TCP + HTTP)
        "fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt" # wget -o D:
        "[httpd] served 1 request, bye"                # httpd, reached via hostfwd
        "[virtio] rng: virtio-rng at 00:04.0"          # stage 13: virtio/
        "[virtio] hvc0: virtio-console at 00:05.0"
        "[virtio] gpu: virtio-gpu 2D at 00:03.0, display 1280x800, framebuffer 1024x768"
        "[virtio] fbcon: text console on the framebuffer, 128x48 characters"
        "crw-rw----      <DEV>  fb0"                   # ls -l /dev (Linux modes since stage 19)
        "crw-------      <DEV>  hvc0"
        "tile: 4 windows on 1024x768, console 61x46"   # the tiling demo
        "console: full screen, 128x48 characters"      # tile -r
        "[forktest] all checks passed"                 # stage 14: processes
        "[forktest args] all checks passed"            # exec'd with argv/envp/auxv
        "[cowtest] all checks passed"
        "[mmaptest] all checks passed"
        "[sigtest] all checks passed"
        "(start spin 1000), exit code 143"             # kill %1 (SIGTERM)
        "15) SIGTERM"                                  # kill -l
        "[threads] all checks passed"                  # stage 15: libc plumbing
        "[echoserver] all checks passed"
        "[ptytest] all checks passed"
        "[nbtest] all checks passed"
        "[fputest] all checks passed"                  # stage 15.1: musl prerequisites
        "[pathtest] all checks passed"
        "rawabi: uname: Rustbelt $KERNEL_VERSION"      # a no-libc gcc binary
        "rawabi: all checks passed"
        "sh: nosuchprogram: command not found"
        "cd: there is no drive Q:"
        "A:/tmp $ pwd"
    )
fi
if [ "$code" = 33 ]; then
    if [ -n "${EXTRA_EXPECT:-}" ]; then
        while IFS= read -r line; do
            case $line in ''|'#'*) continue ;; esac
            line=${line//@KERNEL_VERSION@/$KERNEL_VERSION}
            line=${line//@BIN_COUNT@/$BIN_COUNT}
            line=${line//@ROOT_BIN_COUNT@/${ROOT_BIN_COUNT:-}}
            line=${line//@KERNEL_BANNER@/$KERNEL_BANNER}
            expect+=("$line")
        done < "$EXTRA_EXPECT"
    fi
    missing=0
    for line in "${expect[@]}"; do
        grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }
    done
    # Pipe results (v1): `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> $BIN_COUNT (the programs),
    # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3,
    # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and
    # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000. v2: see above.
    for n in "${PIPE_RESULTS[@]}"; do
        grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }
    done
    # No memory leaks: once the shell and all ~60 programs it ran have
    # exited, exactly as many frames must be in use as before it started.
    if grep -qE "$FRAMES_RE" "$LOG"; then
        echo "[run-headless] frames in use returned to the baseline"
    else
        echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1
    fi
    # The other direction: the host fetched a page from httpd in the guest.
    if grep -qF "Hello from Rustbelt! Rustbelt $KERNEL_VERSION" "$CURL_OUT" 2>/dev/null; then
        echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")"
    else
        echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1
    fi
    # random -n 8: 16 hex digits; random -d 6 5: five numbers 1-6.
    grep -qxE -- "[0-9a-f]{16}" "$LOG" || { echo "[run-headless] MISSING: random -n 8 output"; missing=1; }
    grep -qxE -- "[1-6]( [1-6]){4}" "$LOG" || { echo "[run-headless] MISSING: random -d 6 5 output"; missing=1; }
    # hvc0 is a tty now: its output turns "\n" into "\r\n" (ONLCR), so
    # drop the '\r's before comparing whole lines.
    tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT"
    # The shell on hvc0 ran the host's commands and answered over hvc0.
    # Foundry OS's getty greets differently from the kernel repo's own getty
    # (test-reference passes GETTY_GREETING="Rustbelt getty: ...").
    for line in "${GETTY_GREETING:-Foundry OS getty: shell on this terminal}" "Rustbelt $KERNEL_VERSION ($STAGE" \
                "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do
        grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; }
    done
    grep -qxE -- "[0-9]+ [0-9]+ [0-9]+" "$HVC_OUT" || { echo "[run-headless] MISSING on hvc0: random -d 20 3"; missing=1; }
    if [ "$CONTRACT" = v2 ]; then
        # init adopted and reaped at least one orphan (c/orphan + v2-test.sh make some).
        if grep -qE '^\[init\] self-test finished: exit code 0; init reaped [1-9][0-9]* adopted' "$LOG"; then
            echo "[run-headless] $(grep -E '^\[init\] self-test finished' "$LOG")"
        else
            echo "[run-headless] MISSING: init reaped >= 1 orphan and the self-test exited 0"; missing=1
        fi
    fi
    [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+12 expected shell and hvc0 lines found" || code=35
fi

# isa-debug-exit: QEMU exit status = (value << 1) | 1, so Success(0x10) -> 33.
case $code in
    33)  echo "[run-headless] kernel exited OK"; exit 0 ;;
    35)  echo "[run-headless] kernel reported FAILURE"; exit 1 ;;
    124) echo "[run-headless] timed out (kernel hung?)"; exit 124 ;;
    *)   echo "[run-headless] QEMU exited with $code"; exit "$code" ;;
esac