kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

exec_vm.sh (11874B)


      1 # test/lib/exec_vm.sh — VM execution backend for test/lib/exec_target.sh.
      2 #
      3 # Sourced by exec_target.sh. Provides the `<arch>-freebsd` / `<arch>-windows`
      4 # runner: stateful VMs (expensive to boot) rather than the stateless
      5 # podman/qemu-user runners. exec_target's stateless flush would reboot a VM on
      6 # every call, so this file adds a lifecycle:
      7 #
      8 #   exec_vm_setup TAG        boot the VM for TAG if it is not already reachable,
      9 #                            remembering whether WE booted it (idempotent; a VM
     10 #                            booted for one tag is reused by sibling tags — the
     11 #                            single Windows VM serves both x64 and aarch64).
     12 #   exec_vm_flush_tag TAG K… run the queued exes (indices K into exec_target's
     13 #                            arrays) in ONE VM session and write each case's
     14 #                            .rc/.out/.err. Lazily calls exec_vm_setup, so the VM
     15 #                            stays warm across flushes.
     16 #   exec_vm_supported TAG    true if a runner (qemu + a provisioned/ reachable
     17 #                            VM) exists for TAG on this host.
     18 #   exec_vm_teardown_all     shut down every VM WE booted (install at suite end).
     19 #
     20 # Transport is scripts/{freebsd,windows}_vm.sh `run-batch <arch> <stage> <res>`:
     21 # ship a staging dir in, run its run-remote.{sh,ps1} entry script (authored
     22 # here), and bring each binary's <id>.{rc,out,err} back into <res>. The VM scripts
     23 # stay pure transport; this file owns the protocol + lifecycle.
     24 
     25 EXEC_VM_SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../scripts" && pwd)"
     26 EXEC_VM_ROOT="$(cd "$EXEC_VM_SCRIPTS/.." && pwd)"
     27 EXEC_VM_WORK="${EXEC_VM_WORK:-${TMPDIR:-/tmp}/kit-exec-vm}"
     28 # Space-joined list of VM ids WE booted (so teardown stops only those, never a
     29 # VM the user already had running).
     30 EXEC_VM_STARTED=""
     31 
     32 # ---- tag parsing -----------------------------------------------------------
     33 # Tags are "<arch>-<os>"; os is the last '-' field. arch tokens are tolerant:
     34 # x64/amd64, aa64/aarch64, rv64/riscv64.
     35 _exec_vm_os()   { printf '%s' "${1##*-}"; }
     36 _exec_vm_arch() { printf '%s' "${1%-*}"; }
     37 
     38 # Map an exec arch token to the arch name the VM scripts expect.
     39 #   freebsd: amd64 / aarch64 / riscv64   windows: x64 / aarch64
     40 _exec_vm_vmarch() {
     41     local os="$1" arch="$2"
     42     case "$os" in
     43         freebsd)
     44             case "$arch" in
     45                 x64|amd64|x86_64) echo amd64 ;;
     46                 aa64|aarch64|arm64) echo aarch64 ;;
     47                 rv64|riscv64) echo riscv64 ;;
     48                 *) return 1 ;;
     49             esac ;;
     50         windows)
     51             case "$arch" in
     52                 x64|amd64|x86_64) echo x64 ;;
     53                 aa64|aarch64|arm64) echo aarch64 ;;
     54                 *) return 1 ;;
     55             esac ;;
     56         *) return 1 ;;
     57     esac
     58 }
     59 
     60 # VM identity for a tag: one Windows VM serves both arches; FreeBSD is per-arch.
     61 _exec_vm_id() {
     62     local os arch vmarch
     63     os="$(_exec_vm_os "$1")"; arch="$(_exec_vm_arch "$1")"
     64     if [ "$os" = windows ]; then echo windows; return; fi
     65     vmarch="$(_exec_vm_vmarch "$os" "$arch")" || return 1
     66     echo "freebsd-$vmarch"
     67 }
     68 
     69 _exec_vm_fbsd_qemu() {
     70     case "$1" in amd64) echo qemu-system-x86_64 ;; *) echo "qemu-system-$1" ;; esac
     71 }
     72 
     73 # True if the VM for (os, vmarch) already answers.
     74 _exec_vm_reachable() {
     75     local os="$1" vmarch="$2"
     76     if [ "$os" = freebsd ]; then
     77         "$EXEC_VM_SCRIPTS/freebsd_vm.sh" ssh "$vmarch" true >/dev/null 2>&1
     78     else
     79         "$EXEC_VM_SCRIPTS/windows_vm.sh" ssh aarch64 ver >/dev/null 2>&1
     80     fi
     81 }
     82 
     83 _exec_vm_win_marker() {
     84     local cache="${KIT_WINDOWS_VM_CACHE:-${KIT_WINDOWS_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/kit}/windows-vm}"
     85     printf '%s/win11-arm64.provisioned' "$cache"
     86 }
     87 
     88 # ---- supported -------------------------------------------------------------
     89 exec_vm_supported() {
     90     local os arch vmarch
     91     os="$(_exec_vm_os "$1")"; arch="$(_exec_vm_arch "$1")"
     92     vmarch="$(_exec_vm_vmarch "$os" "$arch")" || return 1
     93     if [ "$os" = freebsd ]; then
     94         command -v "$(_exec_vm_fbsd_qemu "$vmarch")" >/dev/null 2>&1 || return 1
     95         [ -f "$EXEC_VM_ROOT/build/freebsd-vm/images/freebsd-$vmarch.provisioned" ] && return 0
     96         # A durable golden disk in the cache is bootable via `freebsd_vm.sh run`
     97         # even when the working disk / build marker is absent (e.g. after a clean
     98         # build). exec_vm_setup restores + boots it lazily.
     99         ls "${XDG_CACHE_HOME:-$HOME/.cache}"/kit/freebsd-vm/*/freebsd-"$vmarch"-golden.qcow2 \
    100             >/dev/null 2>&1 && return 0
    101         _exec_vm_reachable freebsd "$vmarch" && return 0
    102         return 1
    103     fi
    104     command -v "${KIT_WINDOWS_QEMU:-qemu-system-aarch64}" >/dev/null 2>&1 || return 1
    105     [ -f "$(_exec_vm_win_marker)" ] && return 0
    106     _exec_vm_reachable windows aarch64 && return 0
    107     return 1
    108 }
    109 
    110 # ---- lifecycle -------------------------------------------------------------
    111 exec_vm_setup() {
    112     local os arch vmarch vmid
    113     os="$(_exec_vm_os "$1")"; arch="$(_exec_vm_arch "$1")"
    114     vmarch="$(_exec_vm_vmarch "$os" "$arch")" || return 1
    115     vmid="$(_exec_vm_id "$1")" || return 1
    116     case " $EXEC_VM_STARTED " in *" $vmid "*) return 0 ;; esac
    117     _exec_vm_reachable "$os" "$vmarch" && return 0
    118     mkdir -p "$EXEC_VM_WORK"
    119     if [ "$os" = freebsd ]; then
    120         "$EXEC_VM_SCRIPTS/freebsd_vm.sh" run "$vmarch" \
    121             > "$EXEC_VM_WORK/$vmid.log" 2>&1 &
    122         echo "$!" > "$EXEC_VM_WORK/$vmid.pid"
    123         "$EXEC_VM_SCRIPTS/freebsd_vm.sh" wait-ssh "$vmarch" \
    124             > "$EXEC_VM_WORK/$vmid.wait" 2>&1 || return 1
    125     else
    126         "$EXEC_VM_SCRIPTS/windows_vm.sh" boot \
    127             > "$EXEC_VM_WORK/$vmid.log" 2>&1 || return 1
    128         "$EXEC_VM_SCRIPTS/windows_vm.sh" wait-ssh 900 \
    129             > "$EXEC_VM_WORK/$vmid.wait" 2>&1 || return 1
    130     fi
    131     EXEC_VM_STARTED="$EXEC_VM_STARTED $vmid"
    132     return 0
    133 }
    134 
    135 exec_vm_teardown_all() {
    136     [ "${EXEC_VM_KEEP_UP:-0}" = 1 ] && return 0
    137     local vmid vmarch pid
    138     for vmid in $EXEC_VM_STARTED; do
    139         case "$vmid" in
    140             windows)
    141                 "$EXEC_VM_SCRIPTS/windows_vm.sh" stop </dev/null >/dev/null 2>&1 || true ;;
    142             freebsd-*)
    143                 vmarch="${vmid#freebsd-}"
    144                 # Best-effort guest-side flush only — do NOT wait on a full
    145                 # poweroff: the unprivileged `kit` ssh user may not be allowed
    146                 # to run shutdown(8), so waiting on it just stalls teardown for
    147                 # the whole 30s timeout. (</dev/null so this ssh can't read the
    148                 # caller's stdin — e.g. the EXIT-trap's inherited terminal.)
    149                 "$EXEC_VM_SCRIPTS/freebsd_vm.sh" ssh "$vmarch" 'sync' \
    150                     </dev/null >/dev/null 2>&1 || true
    151                 if [ -f "$EXEC_VM_WORK/$vmid.pid" ]; then
    152                     pid="$(cat "$EXEC_VM_WORK/$vmid.pid")"
    153                     # qemu is our exec'd child (run_arch execs it, so the saved
    154                     # pid IS qemu): SIGTERM makes it quit promptly. Wait a few
    155                     # seconds, then SIGKILL as a backstop.
    156                     kill "$pid" 2>/dev/null || true
    157                     for _ in $(seq 1 5); do
    158                         kill -0 "$pid" 2>/dev/null || break; sleep 1
    159                     done
    160                     kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true
    161                     wait "$pid" 2>/dev/null || true
    162                 fi ;;
    163         esac
    164     done
    165     EXEC_VM_STARTED=""
    166 }
    167 
    168 # ---- guest entry scripts (authored here; run-batch just executes them) ------
    169 _exec_vm_write_sh() {
    170     local stage="$1" ids="$2"
    171     {
    172         echo '#!/bin/sh'
    173         echo 'cd "$(dirname "$0")" || exit 99'
    174         echo 'mkdir -p res'
    175         printf 'for id in%s; do\n' "$ids"
    176         echo '  chmod +x "./$id" 2>/dev/null'
    177         echo '  "./$id" > "res/$id.out" 2> "res/$id.err"'
    178         echo '  echo $? > "res/$id.rc"'
    179         echo 'done'
    180         echo 'exit 0'
    181     } > "$stage/run-remote.sh"
    182 }
    183 
    184 # Capture via Start-Process -PassThru .ExitCode: a launch Windows blocks (e.g. a
    185 # Defender PUA false-positive) does NOT update $LASTEXITCODE, so the bare-`&`
    186 # form would report the previous binary's code. Start-Process throws on a blocked
    187 # launch -> we record rc 126 + a note so the case fails on its own merits.
    188 _exec_vm_write_ps1() {
    189     local stage="$1" ids="$2" id
    190     {
    191         echo '$ErrorActionPreference = "Continue"'
    192         echo 'Set-Location -LiteralPath $PSScriptRoot'
    193         echo '$res = Join-Path $PSScriptRoot "res"'
    194         echo 'New-Item -ItemType Directory -Force -Path $res | Out-Null'
    195         for id in $ids; do
    196             printf 'try { $p = Start-Process -FilePath ".\\%s.exe" -Wait -PassThru -WindowStyle Hidden -RedirectStandardOutput "$res\\%s.out" -RedirectStandardError "$res\\%s.err"; $p.ExitCode | Out-File -Encoding ascii "$res\\%s.rc" } catch { "126" | Out-File -Encoding ascii "$res\\%s.rc"; "launch blocked (Defender?)" | Out-File -Encoding ascii "$res\\%s.err" }\n' \
    197                 "$id" "$id" "$id" "$id" "$id" "$id"
    198         done
    199         echo 'exit 0'
    200     } > "$stage/run-remote.ps1"
    201 }
    202 
    203 # ---- flush -----------------------------------------------------------------
    204 # exec_vm_flush_tag TAG K…  : K are indices into exec_target's EXEC_TARGET_*
    205 # arrays. Stage those exes, run them in the VM, write each case's rc/out/err.
    206 exec_vm_flush_tag() {
    207     local tag="$1"; shift
    208     local os arch vmarch ext stage res ids="" i k id raw
    209     os="$(_exec_vm_os "$tag")"; arch="$(_exec_vm_arch "$tag")"
    210     vmarch="$(_exec_vm_vmarch "$os" "$arch")" || { _exec_vm_mark_all 127 "$@"; return 0; }
    211     ext=""; [ "$os" = windows ] && ext=".exe"
    212 
    213     mkdir -p "$EXEC_VM_WORK"
    214     stage="$(mktemp -d "$EXEC_VM_WORK/stage.XXXXXX")"
    215     res="$stage.res"; mkdir -p "$res"
    216 
    217     i=0
    218     for k in "$@"; do
    219         cp "${EXEC_TARGET_EXES[$k]}" "$stage/$i$ext"
    220         ids="$ids $i"
    221         i=$((i+1))
    222     done
    223     if [ "$os" = windows ]; then _exec_vm_write_ps1 "$stage" "$ids"
    224     else _exec_vm_write_sh "$stage" "$ids"; fi
    225 
    226     if ! exec_vm_setup "$tag"; then
    227         _exec_vm_mark_all 127 "$@"; rm -rf "$stage" "$res"; return 0
    228     fi
    229 
    230     "$EXEC_VM_SCRIPTS/${os}_vm.sh" run-batch "$vmarch" "$stage" "$res" \
    231         > "$stage.batch.out" 2> "$stage.batch.err" || true
    232 
    233     i=0
    234     for k in "$@"; do
    235         if [ -f "$res/$i.rc" ]; then
    236             raw="$(tr -d '\r' < "$res/$i.rc" | head -n1)"
    237             case "$raw" in
    238                 ''|*[!0-9-]*) raw=126 ;;
    239                 # Windows preserves the full 32-bit exit code; mask to 8 bits so
    240                 # .rc is a uniform POSIX-style status across every exec tag.
    241                 *) [ "$os" = windows ] && raw=$(( raw & 255 )) ;;
    242             esac
    243             echo "$raw" > "${EXEC_TARGET_RCS[$k]}"
    244         else
    245             echo 127 > "${EXEC_TARGET_RCS[$k]}"
    246         fi
    247         if [ -f "$res/$i.out" ]; then tr -d '\r' < "$res/$i.out" > "${EXEC_TARGET_OUTS[$k]}"
    248         else : > "${EXEC_TARGET_OUTS[$k]}"; fi
    249         if [ -f "$res/$i.err" ]; then tr -d '\r' < "$res/$i.err" > "${EXEC_TARGET_ERRS[$k]}"
    250         else : > "${EXEC_TARGET_ERRS[$k]}"; fi
    251         i=$((i+1))
    252     done
    253     rm -rf "$stage" "$res"
    254 }
    255 
    256 # Mark every queued case (indices in $2..) with rc $1 and empty out/err.
    257 _exec_vm_mark_all() {
    258     local rc="$1"; shift
    259     local k
    260     for k in "$@"; do
    261         echo "$rc" > "${EXEC_TARGET_RCS[$k]}"
    262         : > "${EXEC_TARGET_OUTS[$k]}"
    263         : > "${EXEC_TARGET_ERRS[$k]}"
    264     done
    265 }
    266 
    267 # Synchronous one-shot for a VM tag (mirrors exec_target_run): sets RUN_RC.
    268 exec_vm_run() {
    269     local tag="$1" exe="$2" out="$3" err="$4"
    270     EXEC_TARGET_EXES=("$exe"); EXEC_TARGET_OUTS=("$out")
    271     EXEC_TARGET_ERRS=("$err"); EXEC_TARGET_RCS=("$EXEC_VM_WORK/.run.rc")
    272     mkdir -p "$EXEC_VM_WORK"
    273     exec_vm_flush_tag "$tag" 0
    274     RUN_RC="$(cat "$EXEC_VM_WORK/.run.rc" 2>/dev/null || echo 127)"
    275     EXEC_TARGET_EXES=(); EXEC_TARGET_OUTS=(); EXEC_TARGET_ERRS=(); EXEC_TARGET_RCS=()
    276 }