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 }