#!/usr/bin/env bash # qemu_screenshot.sh — Boot LithosAnanke under QEMU, wait for ok>, capture framebuffer. # # Usage: # scripts/qemu_screenshot.sh # scripts/qemu_screenshot.sh # # If PRE_BUILT_ISO is supplied the ISO build step is skipped (used by the `qemu` # Makefile target which already built the ISO during the serial acceptance run). # # Outputs: # /screenshot.ppm — raw PPM from QEMU screendump # /qemu-screenshot-YYYYMMDD-HHMMSS.log — serial log # fb/amd64/qemu-screenshot-YYYYMMDD-HHMMSS.png — PNG (tracked in git, not build/) # (amd64-only script; fb/ has one subdirectory per ISA by convention) # # Requires: qemu-system-x86_64, socat, xorriso, mtools, ovmf, imagemagick set -e LOADER_EFI="$1" OVMF_CODE="$2" OVMF_VARS_RO="$3" BUILD_DIR="$4" LOG_DIR="$5" PRE_BUILT_ISO="$6" # optional: skip ISO build if supplied if [ -z "$LOADER_EFI" ] || [ -z "$OVMF_CODE" ] || [ -z "$OVMF_VARS_RO" ] \ || [ -z "$BUILD_DIR" ] || [ -z "$LOG_DIR" ]; then echo "Usage: $0 [PRE_BUILT_ISO]" exit 1 fi for dep in qemu-system-x86_64 socat xorriso mformat; do if ! command -v "$dep" >/dev/null 2>&1; then echo "Error: $dep not found. Install: apt-get install ${dep%%_*}" exit 1 fi done # -------------------------------------------------------------------------- # Build ISO — skipped when a pre-built ISO is provided (e.g. from `qemu` target) # -------------------------------------------------------------------------- if [ -n "$PRE_BUILT_ISO" ]; then QEMU_ISO="$PRE_BUILT_ISO" echo " Using pre-built ISO: ${QEMU_ISO}" else echo " Building ISO for screendump..." mkdir -p "${BUILD_DIR}/iso" dd if=/dev/zero of="${BUILD_DIR}/iso/efi.img" bs=512 count=8192 2>/dev/null mformat -i "${BUILD_DIR}/iso/efi.img" :: mmd -i "${BUILD_DIR}/iso/efi.img" ::/EFI mmd -i "${BUILD_DIR}/iso/efi.img" ::/EFI/BOOT mcopy -i "${BUILD_DIR}/iso/efi.img" "${LOADER_EFI}" ::/EFI/BOOT/BOOTX64.EFI xorriso -as mkisofs -r -J \ -e efi.img -no-emul-boot \ -o "${BUILD_DIR}/starkernel.iso" "${BUILD_DIR}/iso" 2>&1 | grep -v "^xorriso" || true QEMU_ISO="${BUILD_DIR}/starkernel.iso" fi # -------------------------------------------------------------------------- # Setup paths # -------------------------------------------------------------------------- FB_DIR="fb/amd64" mkdir -p "${LOG_DIR}" "${BUILD_DIR}" "${FB_DIR}" TS=$(date +%Y%m%d-%H%M%S) SERIAL_LOG="${LOG_DIR}/qemu-screenshot-${TS}.log" VARS_COPY="${BUILD_DIR}/OVMF_VARS_screenshot.fd" MONITOR_SOCK="${BUILD_DIR}/qemu-monitor-${TS}.sock" PPM_OUT="${BUILD_DIR}/screenshot.ppm" PNG_LOG="${FB_DIR}/qemu-screenshot-${TS}.png" # committed artifact QEMU_PID_FILE="${BUILD_DIR}/qemu-screenshot.pid" cp "${OVMF_VARS_RO}" "${VARS_COPY}" echo " Serial log : ${SERIAL_LOG}" echo " Monitor : ${MONITOR_SOCK}" # -------------------------------------------------------------------------- # Launch QEMU in background with: # -serial file: — capture text output # -monitor unix:,server,nowait — HMP monitor on UNIX socket # -display none — headless; VGA surface still exists # (VGA is the default device on q35; OVMF GOP maps to it) # -------------------------------------------------------------------------- qemu-system-x86_64 \ -machine q35,accel=tcg \ -cpu qemu64 \ -m 1024 \ -drive if=pflash,format=raw,readonly=on,file="${OVMF_CODE}" \ -drive if=pflash,format=raw,file="${VARS_COPY}" \ -cdrom "${QEMU_ISO}" -boot d \ -serial file:"${SERIAL_LOG}" \ -monitor unix:"${MONITOR_SOCK}",server,nowait \ -display none \ -no-reboot \ & QEMU_PID=$! echo "$QEMU_PID" > "${QEMU_PID_FILE}" echo " QEMU PID : ${QEMU_PID}" # -------------------------------------------------------------------------- # Wait for ok> prompt in serial log (up to 120 s) # -------------------------------------------------------------------------- echo " Waiting for ok> prompt..." DEADLINE=$(( $(date +%s) + 1800 )) while true; do if [ "$(date +%s)" -ge "$DEADLINE" ]; then echo " TIMEOUT waiting for ok> — killing QEMU" kill "$QEMU_PID" 2>/dev/null || true exit 1 fi if grep -q "ok>" "${SERIAL_LOG}" 2>/dev/null; then echo " ok> detected!" break fi sleep 1 done # Give the framebuffer a moment to fully paint before capturing sleep 2 # -------------------------------------------------------------------------- # Issue screendump via HMP monitor socket # -------------------------------------------------------------------------- echo "screendump ${PPM_OUT}" | socat - "UNIX-CONNECT:${MONITOR_SOCK}" 2>/dev/null || true sleep 1 # -------------------------------------------------------------------------- # Terminate QEMU # -------------------------------------------------------------------------- echo "quit" | socat - "UNIX-CONNECT:${MONITOR_SOCK}" 2>/dev/null || true sleep 1 kill "$QEMU_PID" 2>/dev/null || true wait "$QEMU_PID" 2>/dev/null || true rm -f "${QEMU_PID_FILE}" "${MONITOR_SOCK}" "${VARS_COPY}" # -------------------------------------------------------------------------- # Convert PPM → PNG # -------------------------------------------------------------------------- if [ -f "${PPM_OUT}" ]; then echo " PPM : ${PPM_OUT} ($(wc -c < "${PPM_OUT}") bytes)" if command -v convert >/dev/null 2>&1; then convert "${PPM_OUT}" "${PNG_LOG}" rm -f "${PPM_OUT}" echo " PNG : ${PNG_LOG}" elif command -v python3 >/dev/null 2>&1 && python3 -c "import PIL" >/dev/null 2>&1; then python3 -c "from PIL import Image; Image.open('${PPM_OUT}').save('${PNG_LOG}')" rm -f "${PPM_OUT}" echo " PNG : ${PNG_LOG} (via python3/PIL)" else echo " WARN: no PPM->PNG converter found (imagemagick 'convert' or python3+PIL) — PPM only" PNG_LOG="${FB_DIR}/$(basename "${PNG_LOG}" .png).ppm" mv "${PPM_OUT}" "${PNG_LOG}" fi echo "" echo "=== Screendump complete ===" echo " Serial log : ${SERIAL_LOG}" echo " Screenshot : ${PNG_LOG}" else echo " ERROR: PPM not written — screendump may have failed" echo " Serial log: ${SERIAL_LOG}" cat "${SERIAL_LOG}" 2>/dev/null | tail -20 exit 1 fi