exec_vm.sh (10956B)
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 _exec_vm_reachable freebsd "$vmarch" && return 0 97 return 1 98 fi 99 command -v "${KIT_WINDOWS_QEMU:-qemu-system-aarch64}" >/dev/null 2>&1 || return 1 100 [ -f "$(_exec_vm_win_marker)" ] && return 0 101 _exec_vm_reachable windows aarch64 && return 0 102 return 1 103 } 104 105 # ---- lifecycle ------------------------------------------------------------- 106 exec_vm_setup() { 107 local os arch vmarch vmid 108 os="$(_exec_vm_os "$1")"; arch="$(_exec_vm_arch "$1")" 109 vmarch="$(_exec_vm_vmarch "$os" "$arch")" || return 1 110 vmid="$(_exec_vm_id "$1")" || return 1 111 case " $EXEC_VM_STARTED " in *" $vmid "*) return 0 ;; esac 112 _exec_vm_reachable "$os" "$vmarch" && return 0 113 mkdir -p "$EXEC_VM_WORK" 114 if [ "$os" = freebsd ]; then 115 "$EXEC_VM_SCRIPTS/freebsd_vm.sh" run "$vmarch" \ 116 > "$EXEC_VM_WORK/$vmid.log" 2>&1 & 117 echo "$!" > "$EXEC_VM_WORK/$vmid.pid" 118 "$EXEC_VM_SCRIPTS/freebsd_vm.sh" wait-ssh "$vmarch" \ 119 > "$EXEC_VM_WORK/$vmid.wait" 2>&1 || return 1 120 else 121 "$EXEC_VM_SCRIPTS/windows_vm.sh" boot \ 122 > "$EXEC_VM_WORK/$vmid.log" 2>&1 || return 1 123 "$EXEC_VM_SCRIPTS/windows_vm.sh" wait-ssh 900 \ 124 > "$EXEC_VM_WORK/$vmid.wait" 2>&1 || return 1 125 fi 126 EXEC_VM_STARTED="$EXEC_VM_STARTED $vmid" 127 return 0 128 } 129 130 exec_vm_teardown_all() { 131 [ "${EXEC_VM_KEEP_UP:-0}" = 1 ] && return 0 132 local vmid vmarch pid 133 for vmid in $EXEC_VM_STARTED; do 134 case "$vmid" in 135 windows) 136 "$EXEC_VM_SCRIPTS/windows_vm.sh" stop >/dev/null 2>&1 || true ;; 137 freebsd-*) 138 vmarch="${vmid#freebsd-}" 139 "$EXEC_VM_SCRIPTS/freebsd_vm.sh" ssh "$vmarch" \ 140 'sync; shutdown -p now' >/dev/null 2>&1 || true 141 if [ -f "$EXEC_VM_WORK/$vmid.pid" ]; then 142 pid="$(cat "$EXEC_VM_WORK/$vmid.pid")" 143 for _ in $(seq 1 30); do 144 kill -0 "$pid" 2>/dev/null || break; sleep 1 145 done 146 kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true 147 sleep 1 148 kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true 149 wait "$pid" 2>/dev/null || true 150 fi ;; 151 esac 152 done 153 EXEC_VM_STARTED="" 154 } 155 156 # ---- guest entry scripts (authored here; run-batch just executes them) ------ 157 _exec_vm_write_sh() { 158 local stage="$1" ids="$2" 159 { 160 echo '#!/bin/sh' 161 echo 'cd "$(dirname "$0")" || exit 99' 162 echo 'mkdir -p res' 163 printf 'for id in%s; do\n' "$ids" 164 echo ' chmod +x "./$id" 2>/dev/null' 165 echo ' "./$id" > "res/$id.out" 2> "res/$id.err"' 166 echo ' echo $? > "res/$id.rc"' 167 echo 'done' 168 echo 'exit 0' 169 } > "$stage/run-remote.sh" 170 } 171 172 # Capture via Start-Process -PassThru .ExitCode: a launch Windows blocks (e.g. a 173 # Defender PUA false-positive) does NOT update $LASTEXITCODE, so the bare-`&` 174 # form would report the previous binary's code. Start-Process throws on a blocked 175 # launch -> we record rc 126 + a note so the case fails on its own merits. 176 _exec_vm_write_ps1() { 177 local stage="$1" ids="$2" id 178 { 179 echo '$ErrorActionPreference = "Continue"' 180 echo 'Set-Location -LiteralPath $PSScriptRoot' 181 echo '$res = Join-Path $PSScriptRoot "res"' 182 echo 'New-Item -ItemType Directory -Force -Path $res | Out-Null' 183 for id in $ids; do 184 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' \ 185 "$id" "$id" "$id" "$id" "$id" "$id" 186 done 187 echo 'exit 0' 188 } > "$stage/run-remote.ps1" 189 } 190 191 # ---- flush ----------------------------------------------------------------- 192 # exec_vm_flush_tag TAG K… : K are indices into exec_target's EXEC_TARGET_* 193 # arrays. Stage those exes, run them in the VM, write each case's rc/out/err. 194 exec_vm_flush_tag() { 195 local tag="$1"; shift 196 local os arch vmarch ext stage res ids="" i k id raw 197 os="$(_exec_vm_os "$tag")"; arch="$(_exec_vm_arch "$tag")" 198 vmarch="$(_exec_vm_vmarch "$os" "$arch")" || { _exec_vm_mark_all 127 "$@"; return 0; } 199 ext=""; [ "$os" = windows ] && ext=".exe" 200 201 mkdir -p "$EXEC_VM_WORK" 202 stage="$(mktemp -d "$EXEC_VM_WORK/stage.XXXXXX")" 203 res="$stage.res"; mkdir -p "$res" 204 205 i=0 206 for k in "$@"; do 207 cp "${EXEC_TARGET_EXES[$k]}" "$stage/$i$ext" 208 ids="$ids $i" 209 i=$((i+1)) 210 done 211 if [ "$os" = windows ]; then _exec_vm_write_ps1 "$stage" "$ids" 212 else _exec_vm_write_sh "$stage" "$ids"; fi 213 214 if ! exec_vm_setup "$tag"; then 215 _exec_vm_mark_all 127 "$@"; rm -rf "$stage" "$res"; return 0 216 fi 217 218 "$EXEC_VM_SCRIPTS/${os}_vm.sh" run-batch "$vmarch" "$stage" "$res" \ 219 > "$stage.batch.out" 2> "$stage.batch.err" || true 220 221 i=0 222 for k in "$@"; do 223 if [ -f "$res/$i.rc" ]; then 224 raw="$(tr -d '\r' < "$res/$i.rc" | head -n1)" 225 case "$raw" in 226 ''|*[!0-9-]*) raw=126 ;; 227 # Windows preserves the full 32-bit exit code; mask to 8 bits so 228 # .rc is a uniform POSIX-style status across every exec tag. 229 *) [ "$os" = windows ] && raw=$(( raw & 255 )) ;; 230 esac 231 echo "$raw" > "${EXEC_TARGET_RCS[$k]}" 232 else 233 echo 127 > "${EXEC_TARGET_RCS[$k]}" 234 fi 235 if [ -f "$res/$i.out" ]; then tr -d '\r' < "$res/$i.out" > "${EXEC_TARGET_OUTS[$k]}" 236 else : > "${EXEC_TARGET_OUTS[$k]}"; fi 237 if [ -f "$res/$i.err" ]; then tr -d '\r' < "$res/$i.err" > "${EXEC_TARGET_ERRS[$k]}" 238 else : > "${EXEC_TARGET_ERRS[$k]}"; fi 239 i=$((i+1)) 240 done 241 rm -rf "$stage" "$res" 242 } 243 244 # Mark every queued case (indices in $2..) with rc $1 and empty out/err. 245 _exec_vm_mark_all() { 246 local rc="$1"; shift 247 local k 248 for k in "$@"; do 249 echo "$rc" > "${EXEC_TARGET_RCS[$k]}" 250 : > "${EXEC_TARGET_OUTS[$k]}" 251 : > "${EXEC_TARGET_ERRS[$k]}" 252 done 253 } 254 255 # Synchronous one-shot for a VM tag (mirrors exec_target_run): sets RUN_RC. 256 exec_vm_run() { 257 local tag="$1" exe="$2" out="$3" err="$4" 258 EXEC_TARGET_EXES=("$exe"); EXEC_TARGET_OUTS=("$out") 259 EXEC_TARGET_ERRS=("$err"); EXEC_TARGET_RCS=("$EXEC_VM_WORK/.run.rc") 260 mkdir -p "$EXEC_VM_WORK" 261 exec_vm_flush_tag "$tag" 0 262 RUN_RC="$(cat "$EXEC_VM_WORK/.run.rc" 2>/dev/null || echo 127)" 263 EXEC_TARGET_EXES=(); EXEC_TARGET_OUTS=(); EXEC_TARGET_ERRS=(); EXEC_TARGET_RCS=() 264 }