RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / scripts / screenshot.sh
4528 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
#!/usr/bin/env bash
# Boot the normal (interactive) kernel with no window, type a shell session
# into it through the QEMU monitor's `sendkey` command, then dump the
# screen (the virtio-gpu framebuffer) to a PNG, or a PPM if the file name
# ends in .ppm (scripts/gpu-check.sh reads its pixels). Doubles as an automated test of the keyboard, the raw
# console mode and the shell's line editor (Backspace, up-arrow history):
# afterwards the serial copy of the console is checked for expected text.
#
# Usage: ./scripts/screenshot.sh [out.png|out.ppm] [session.txt] [expected text...]
# session.txt: one command per line, typed key by key followed by Enter.
# In it, {BS} is Backspace and {UP} is the up arrow.
# Exits 1 if any expected text doesn't show up.
set -euo pipefail
cd "$(dirname "$0")/.."

cargo build
# Force bootimage to repack the disk image (see scripts/runner.sh for why).
touch target/x86_64-rust_kernel/debug/rustbelt
cargo bootimage
IMG=target/x86_64-rust_kernel/debug/bootimage-rustbelt.bin
OUT="${1:-$PWD/boot.png}"
SESSION="${2:-}"
shift $(( $# > 2 ? 2 : $# ))
if [ -z "$SESSION" ]; then
    # The default session (what boot.png shows): the tiling demo moves the
    # text console into the left window of the virtio-gpu framebuffer, then
    # a few commands run in it: the devices, random numbers from virtio-rng,
    # and a ping. "vx{BS}er" types a typo and fixes it.
    SESSION=$(mktemp /tmp/session.XXXXXX)
    cat > "$SESSION" <<'TXT'
tile
vx{BS}er
ls -l /dev
random
random -d 6 5
ping -c 2 -i 200 10.0.2.2
TXT
    set -- "Rustbelt 0.13.0 (stage 13: virtual drivers) x86_64" "tile: 4 windows on 1024x768, console 61x46" \
        "<DEV>  fb0" "2 packets transmitted, 2 received, 0% packet loss"
fi
SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX)
SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)

qemu-system-x86_64 -smp 4 -drive format=raw,file="$IMG" \
    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \
    -drive file=target/ext2.img,format=raw,if=virtio,snapshot=on \
    -drive file=target/exfat.img,format=raw,if=virtio,snapshot=on \
    -drive file=target/btrfs.img,format=raw,if=virtio,snapshot=on \
    -netdev "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh" \
    -device virtio-net-pci,netdev=net0 \
    -vga none -device virtio-vga -device virtio-rng-pci \
    -device virtio-serial-pci -device virtconsole,chardev=hvc -chardev null,id=hvc \
    -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait &
QPID=$!
sleep 4   # boot until the shell prompt

# Talk to the QEMU monitor over its UNIX socket (python avoids needing socat).
python3 - "$SOCK" "$OUT" "$SESSION" <<'PY'
import re, socket, sys, time
sock, out, session = sys.argv[1:]
# QEMU key names for the characters we might type (US keyboard layout).
names = {' ': 'spc', ',': 'comma', '.': 'dot', '/': 'slash', '-': 'minus', '=': 'equal',
         ':': 'shift-semicolon', '|': 'shift-backslash', '>': 'shift-dot', '<': 'shift-comma',
         '"': 'shift-apostrophe', "'": 'apostrophe', '_': 'shift-minus', '!': 'shift-1',
         '#': 'shift-3', '&': 'shift-7', '{BS}': 'backspace', '{UP}': 'up', '\n': 'ret'}
def key(c):
    if c in names: return names[c]
    if c.isupper(): return 'shift-' + c.lower()
    return c
s = socket.socket(socket.AF_UNIX); s.connect(sock)
for line in open(session):
    for c in re.findall(r'\{[A-Z]+\}|.|\n', line, re.S):
        s.sendall(f"sendkey {key(c)}\n".encode()); time.sleep(0.08)
    time.sleep(0.8)   # let the command run
time.sleep(0.5)
fmt = 'ppm' if out.endswith('.ppm') else 'png'
s.sendall(f"screendump {out} -f {fmt}\n".encode()); time.sleep(1)
s.sendall(b"quit\n"); time.sleep(0.5)
PY
wait $QPID || true
echo "---- serial output ----"
cat -v "$SERIAL_LOG"; echo   # cat -v shows Backspace as ^H
echo "-----------------------"
# Replay the serial output like a terminal would (\b = erase one character)
# and check the expected text is there.
if python3 - "$SERIAL_LOG" "$@" <<'PY'
import sys
data = open(sys.argv[1], 'rb').read().decode(errors='replace').replace('\x08 \x08', '\x08')
out = []
for ch in data:
    if ch == '\x08':
        if out and out[-1] != '\n': out.pop()
    else:
        out.append(ch)
text = ''.join(out)
missing = [e for e in sys.argv[2:] if e not in text]
for e in missing: print(f"[screenshot] MISSING: {e}")
sys.exit(1 if missing else 0)
PY
then
    echo "[screenshot] session OK: all $# expected texts found"
else
    rm -f "$SERIAL_LOG"
    exit 1
fi
rm -f "$SERIAL_LOG"
echo "[screenshot] saved $OUT"