kit

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

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 }