macOS: drive the install and all customization from a Recovery "PE", no GUI
Replace the screenshot/OCR/keystroke driving of Apple's Recovery with a "PE": BaseSystem.dmg (a journaled HFS+ volume, writable from Linux) with one LaunchDaemon added (makeRecoveryPE) that runs /Volumes/VMIX/run.sh as root at boot, records the status and powers off. launchd loads it alongside its signed cache (verified on Tahoe 26.6.2); same idea as AutoNBI/Imagr NetBoot images. - makeImage: the PE runs vmix-install.sh (erase, installer app, SharedSupport pkgdmg, startosinstall). Progress is read from the serial console (boot-args serial=3 -v, VMIX-* markers) and screenshots (brightness only). Fully offline; prepare now takes ~5 min instead of ~10. - customizeImage: boots the PE with the image attached and runs the template offline against the mounted System/Data volumes; OpenCore ScanPolicy restricted to HFS+/SATA so only the PE can boot. One PE boot ~30 s. The installed macOS is never booted for customization, so nothing depends on launchd/BTM approval or a first-boot agent (removed). - templates rewritten for offline use: generalize creates the user with dscl -f (admin, home, auto-login kcpassword, Setup Assistant suppression, hostname, locale, timezone, keyboard type, container resize); remote-access, no-updates, performance edit the target's plists. - makeBootDisk: build-time OpenCore variant (serial console, ScanPolicy). - vm-driver.py rewritten: passive observation only (serial markers, kernel boots, panics, brightness), disk+serial-aware hang watchdog, reboot-death reset, halt/loginwindow detection. No OCR/tesseract. - OpenCore: four SMBIOS DIMMs for MacPro7,1 (no "Memory Modules Misconfigured" warning). - tools/soak.sh: repeatability harness. Verified on daku: base install 23 min end to end; basic + generalize in three ~30 s PE boots; the result auto-logs into the desktop with the created user. Root cause of the "first-boot hang" (from the serial log): the guest's restart path panics (IOPlatformHaltRestartAction -> AppleSMC, SMCWDT smcWriteKey kSMCBadCommand, nested panic) because the pinned OSX-KVM Lilu disables itself on macOS 26, so VirtualSMC never loads. Handled by the driver (reset within 60 s); kext update to follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
This commit is contained in:
parent
58a317f5d2
commit
8dc8f4265d
24 changed files with 802 additions and 977 deletions
|
|
@ -1,42 +0,0 @@
|
|||
#!/bin/sh
|
||||
# vmix agent: LaunchDaemon that runs at every boot as root (installed by the vmix
|
||||
# agent pkg via startosinstall --installpackage). If a volume named VMIX carrying
|
||||
# vmix-run.sh is attached, run it, record the result on the volume and power off.
|
||||
# Without the volume it is a no-op (normal boot). Counterpart of the Windows Audit
|
||||
# Mode RunOnce script; the generalize step removes it once the image is sealed.
|
||||
LOG=/var/log/vmix-agent.log
|
||||
exec >>"$LOG" 2>&1
|
||||
echo "=== vmix agent: $(date) ==="
|
||||
# The agent pkg bootstraps this daemon during the OS install (to approve it past
|
||||
# Background Task Management, so launchd runs it at first boot). Don't do the job
|
||||
# in that installer environment — only on the installed system's first boot.
|
||||
if pgrep -x bootinstalld >/dev/null 2>&1 || pgrep -qx "Installer Progress" 2>/dev/null \
|
||||
|| [ -d /System/Volumes/Update/mnt1 ]; then
|
||||
echo "vmix agent: OS installer is running, skipping"
|
||||
exit 0
|
||||
fi
|
||||
# let DiskArbitration settle so the VMIX volume is mountable
|
||||
sleep 5
|
||||
V=/Volumes/VMIX
|
||||
i=0
|
||||
while [ ! -f "$V/vmix-run.sh" ] && [ $i -lt 30 ]; do
|
||||
diskutil mount VMIX >/dev/null 2>&1
|
||||
sleep 2
|
||||
i=$((i + 1))
|
||||
done
|
||||
if [ ! -f "$V/vmix-run.sh" ]; then
|
||||
echo "vmix agent: no VMIX volume, normal boot"
|
||||
exit 0
|
||||
fi
|
||||
echo "vmix agent: running vmix-run.sh"
|
||||
cd "$V" || exit 1
|
||||
sh "$V/vmix-run.sh" >"$V/vmix-run.log" 2>&1
|
||||
rc=$?
|
||||
echo "vmix agent: vmix-run.sh exited $rc"
|
||||
echo "$rc" >"$V/vmix-run.status"
|
||||
cp "$LOG" "$V/vmix-agent.log" 2>/dev/null
|
||||
cp /var/log/vmix-agent-install.log "$V/vmix-agent-install.log" 2>/dev/null
|
||||
sync
|
||||
sleep 2
|
||||
diskutil unmount force "$V" >/dev/null 2>&1
|
||||
shutdown -h now
|
||||
|
|
@ -3,17 +3,17 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>ch.vmix.agent</string>
|
||||
<string>ch.vmix.pe</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>/Library/vmix/agent.sh</string>
|
||||
<string>/bin/bash</string>
|
||||
<string>/usr/libexec/vmix/pe.sh</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/var/log/vmix-agent.log</string>
|
||||
<string>/dev/console</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/var/log/vmix-agent.log</string>
|
||||
<string>/dev/console</string>
|
||||
</dict>
|
||||
</plist>
|
||||
51
lib/images/macos/guest/pe-lib.sh
Normal file
51
lib/images/macos/guest/pe-lib.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# vmix PE helpers, sourced by run.sh scripts running in the recovery.
|
||||
# Expects V=/Volumes/VMIX (set by pe.sh) and VOLUME_NAME from vmix.conf.
|
||||
V=${V:-/Volumes/VMIX}
|
||||
[ -f "$V/vmix.conf" ] && . "$V/vmix.conf"
|
||||
VOLUME_NAME=${VOLUME_NAME:-Macintosh HD}
|
||||
|
||||
pe_log() { echo "VMIX: $*"; }
|
||||
pe_fail() { echo "VMIX-FAIL: $*"; exit 1; }
|
||||
|
||||
# Mount the installed system's APFS volume group (System read-only, Data rw) and
|
||||
# export SYS / DATA mount points plus SYS_ID / DATA_ID device identifiers.
|
||||
pe_mount_target() {
|
||||
local list; list=$(diskutil list)
|
||||
DATA_ID=$(echo "$list" | awk -v n="APFS Volume $VOLUME_NAME - Data" 'index($0, n) {print $NF; exit}')
|
||||
SYS_ID=$(echo "$list" | awk -v n="APFS Volume $VOLUME_NAME " '!/ - Data/ && index($0, n) {print $NF; exit}')
|
||||
[ -n "$DATA_ID" ] && [ -n "$SYS_ID" ] || { pe_log "target volumes not found"; echo "$list"; return 1; }
|
||||
diskutil mount "$SYS_ID" >/dev/null 2>&1 || true
|
||||
diskutil mount "$DATA_ID" >/dev/null 2>&1 || true
|
||||
SYS=$(diskutil info "$SYS_ID" | sed -n 's/^ *Mount Point: *//p')
|
||||
DATA=$(diskutil info "$DATA_ID" | sed -n 's/^ *Mount Point: *//p')
|
||||
[ -d "$DATA/private/var/db" ] || { pe_log "Data volume not mounted (SYS=[$SYS] DATA=[$DATA])"; return 1; }
|
||||
pe_log "target mounted: SYS=[$SYS] DATA=[$DATA]"
|
||||
export SYS DATA SYS_ID DATA_ID
|
||||
}
|
||||
|
||||
pe_unmount_target() {
|
||||
sync
|
||||
diskutil unmount "$DATA_ID" >/dev/null 2>&1 || true
|
||||
diskutil unmount "$SYS_ID" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# plist helpers on files of the (offline) target: create the file if missing.
|
||||
pe_plist_set() { # FILE KEYPATH TYPE VALUE (TYPE: string|bool|integer|float)
|
||||
local f=$1 k=$2 t=$3 v=$4
|
||||
[ -f "$f" ] || plutil -create xml1 "$f"
|
||||
plutil -replace "$k" "-$t" "$v" "$f"
|
||||
}
|
||||
pe_plist_dict() { # FILE KEYPATH — make sure a dictionary exists at KEYPATH
|
||||
local f=$1 k=$2
|
||||
[ -f "$f" ] || plutil -create xml1 "$f"
|
||||
plutil -extract "$k" xml1 -o /dev/null "$f" >/dev/null 2>&1 || plutil -insert "$k" -dictionary "$f"
|
||||
}
|
||||
# launchd service override on the target (disabled.plist): pe_service LABEL true|false
|
||||
pe_service_disabled() {
|
||||
local f="$DATA/private/var/db/com.apple.xpc.launchd/disabled.plist"
|
||||
mkdir -p "$(dirname "$f")"
|
||||
pe_plist_set "$f" "$1" bool "$2"
|
||||
}
|
||||
# version of the installed system
|
||||
pe_target_version() { plutil -extract ProductVersion raw -o - "$SYS/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null; }
|
||||
pe_target_build() { plutil -extract ProductBuildVersion raw -o - "$SYS/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null; }
|
||||
40
lib/images/macos/guest/pe.sh
Executable file
40
lib/images/macos/guest/pe.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
# vmix PE hook. Runs as root from launchd when the patched Recovery boots
|
||||
# (injected by makeRecoveryPE). If a VMIX volume is attached it runs
|
||||
# /Volumes/VMIX/run.sh, records the exit status on the volume and powers off;
|
||||
# without one it does nothing and the recovery behaves normally.
|
||||
# Everything printed here goes to /dev/console, i.e. the host's serial log.
|
||||
exec >/dev/console 2>&1
|
||||
echo "VMIX-PE: hook started $(date) uid=$(id -u)"
|
||||
V=/Volumes/VMIX
|
||||
i=0
|
||||
while [ ! -f "$V/run.sh" ] && [ $i -lt 90 ]; do
|
||||
diskutil mount VMIX >/dev/null 2>&1
|
||||
sleep 2; i=$((i + 1))
|
||||
done
|
||||
if [ ! -f "$V/run.sh" ]; then
|
||||
echo "VMIX-PE: no VMIX volume, leaving the recovery alone"
|
||||
exit 0
|
||||
fi
|
||||
echo "VMIX-PE: VMIX mounted after $i retries"
|
||||
caffeinate -dimsu -t 86400 >/dev/null 2>&1 &
|
||||
[ -f "$V/vmix.conf" ] && . "$V/vmix.conf"
|
||||
# certificate checks need a sane clock; a fresh VM RTC can be off
|
||||
[ -n "${BUILD_DATE:-}" ] && date -u "$BUILD_DATE" >/dev/null 2>&1 && echo "VMIX-PE: clock set to $(date -u)"
|
||||
export V
|
||||
cd "$V"
|
||||
echo "VMIX-PE: running run.sh"
|
||||
/bin/bash "$V/run.sh" 2>&1 | tee "$V/vmix-run.log"
|
||||
rc=${PIPESTATUS[0]}
|
||||
echo "$rc" > "$V/vmix-run.status"
|
||||
echo "VMIX-PE: run.sh exited $rc"
|
||||
if [ -f "$V/vmix-reboot" ]; then
|
||||
rm -f "$V/vmix-reboot"; sync
|
||||
echo "VMIX-PE: rebooting as requested"
|
||||
reboot
|
||||
exit 0
|
||||
fi
|
||||
sync; sleep 1
|
||||
diskutil unmount force "$V" >/dev/null 2>&1
|
||||
echo "VMIX-PE-DONE rc=$rc"
|
||||
shutdown -h now
|
||||
|
|
@ -1,88 +1,65 @@
|
|||
#!/bin/sh
|
||||
# vmix: automated macOS install. Runs inside macOS Recovery's Terminal, started
|
||||
# by vm-driver.py which types "sh /Volumes/VMIX/run.sh" for us.
|
||||
#
|
||||
# 1. erase the target disk (found by size) as APFS "Macintosh HD"
|
||||
# 2. rebuild "Install macOS <name>.app": app skeleton from installer-app.tar
|
||||
# (host-extracted Payload) + SharedSupport.dmg = the WHOLE InstallAssistant.pkg
|
||||
# dd'd byte-exact from a raw disk (Apple's own postinstall hardlinks the pkg
|
||||
# there: it is a "pkgdmg" whose koly footer points at the dmg inside; the bare
|
||||
# xar member fails startosinstall with "pkgdmg is missing a footer")
|
||||
# 3. startosinstall unattended, with the vmix agent pkg as --installpackage
|
||||
# 4. startosinstall reboots itself into the install phase; the vmix agent pkg
|
||||
# installs during that phase and runs on the installed system's first boot
|
||||
#
|
||||
# On first boot of the installed system the agent runs /Volumes/VMIX/vmix-run.sh
|
||||
# and powers off, which ends the QEMU session on the host.
|
||||
|
||||
# macOS Recovery invokes us as `sh` (bash in POSIX mode, no process substitution);
|
||||
# re-exec once under bash so `>(tee ...)` and other bashisms work.
|
||||
if [ -z "${VMIX_REEXEC:-}" ]; then VMIX_REEXEC=1 exec bash "$0" "$@"; fi
|
||||
|
||||
V="/Volumes/VMIX"
|
||||
# tee to the Terminal (visible in host screenshots) and to a log on the volume
|
||||
exec > >(tee "$V/install.log") 2>&1
|
||||
#!/bin/bash
|
||||
# vmix unattended macOS install, run by the PE hook (pe.sh) as root in the
|
||||
# Recovery with /Volumes/VMIX mounted (V). Needs vmix.conf: TARGET_BYTES,
|
||||
# PKG_BYTES, PKG_DISK_BYTES, APP_NAME, VOLUME_NAME.
|
||||
# 1. find the target disk and the SharedSupport (InstallAssistant.pkg) disk by size
|
||||
# 2. erase the target as APFS, unpack the installer app, dd the whole pkg into it
|
||||
# as SharedSupport.dmg (a "pkgdmg", startosinstall checks its koly footer)
|
||||
# 3. startosinstall prepares, then reboots itself into the install phase; the
|
||||
# installed system's first boot ends at the loginwindow (the host powers off)
|
||||
# Never returns on success; a return means failure (the PE records the status).
|
||||
set -x
|
||||
. "$V/vmix.conf"
|
||||
# keep the recovery display awake so the host driver can watch the screen
|
||||
caffeinate -dimsu -t 86400 >/dev/null 2>&1 &
|
||||
pmset -a displaysleep 0 sleep 0 >/dev/null 2>&1 || true
|
||||
|
||||
. "$V/pe-lib.sh"
|
||||
fail() {
|
||||
echo "vmix-install: FAIL: $*"
|
||||
cp /var/log/install.log "$V/system-install.log" 2>/dev/null || true
|
||||
echo 1 >"$V/install.status"
|
||||
echo "VMIX-FAIL: $*"
|
||||
cp /var/log/install.log "$V/system-install.log" 2>/dev/null
|
||||
sync
|
||||
sleep 2
|
||||
shutdown -h now 2>/dev/null || halt 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
# each boot into the PE with the install still pending is one attempt
|
||||
ATTEMPT=$(( $(cat "$V/install.attempt" 2>/dev/null || echo 0) + 1 ))
|
||||
echo "$ATTEMPT" > "$V/install.attempt"; sync
|
||||
echo "VMIX-INSTALL: attempt $ATTEMPT (boot into the PE)"
|
||||
[ "$ATTEMPT" -le 3 ] || fail "the installer keeps coming back to the PE ($ATTEMPT boots)"
|
||||
|
||||
# whole-disk identifier (diskN) whose size in bytes is exactly $1
|
||||
# --- 1. disks by exact size
|
||||
disk_by_size() {
|
||||
for d in $(diskutil list | grep -oE '^/dev/disk[0-9]+'); do
|
||||
s=$(diskutil info "$d" | sed -n 's/.*Disk Size:.*(\([0-9][0-9]*\) Bytes).*/\1/p')
|
||||
[ "$s" = "$1" ] && { echo "${d#/dev/}"; return 0; }
|
||||
for d in $(diskutil list | grep -oE '^/dev/disk[0-9]+' | sort -u); do
|
||||
if [ "$(diskutil info "$d" | sed -n 's/.*Disk Size:.*(\([0-9]*\) Bytes).*/\1/p')" = "$1" ]; then
|
||||
echo "${d#/dev/}"; return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
TARGET=$(disk_by_size "$TARGET_BYTES") || fail "target disk of $TARGET_BYTES bytes not found"
|
||||
SSDISK=$(disk_by_size "$PKG_DISK_BYTES") || fail "SharedSupport disk of $PKG_DISK_BYTES bytes not found"
|
||||
echo "VMIX-INSTALL: target=$TARGET sharedsupport=$SSDISK"
|
||||
|
||||
echo "vmix-install: $(date) app=$APP_NAME volume=$VOLUME_NAME"
|
||||
TARGET=$(disk_by_size "$TARGET_BYTES") || fail "target disk ($TARGET_BYTES bytes) not found"
|
||||
SSDISK=$(disk_by_size "$PKG_DISK_BYTES") || fail "installer pkg disk ($PKG_DISK_BYTES bytes) not found"
|
||||
echo "vmix-install: target=$TARGET sharedsupport=$SSDISK"
|
||||
|
||||
# --- 1. erase the target disk as an APFS volume
|
||||
diskutil eraseDisk APFS "$VOLUME_NAME" GPT "$TARGET" || fail "eraseDisk $TARGET"
|
||||
# --- 2. target volume + installer app (the pkg payload skeleton + SharedSupport.dmg)
|
||||
VOL="/Volumes/$VOLUME_NAME"
|
||||
[ -d "$VOL" ] || fail "$VOL not mounted"
|
||||
|
||||
# --- 2. rebuild the installer app on the target volume
|
||||
tar -xf "$V/installer-app.tar" -C "$VOL" || fail "untar installer-app.tar"
|
||||
APP="$VOL/$APP_NAME"
|
||||
SOI="$APP/Contents/Resources/startosinstall"
|
||||
[ -x "$SOI" ] || fail "startosinstall missing in $APP"
|
||||
SS="$APP/Contents/SharedSupport/SharedSupport.dmg"
|
||||
mkdir -p "$APP/Contents/SharedSupport"
|
||||
FULL=$((PKG_BYTES / 1048576))
|
||||
REM=$((PKG_BYTES % 1048576))
|
||||
dd if="/dev/r$SSDISK" of="$SS" bs=1048576 count=$FULL || fail "dd SharedSupport.dmg"
|
||||
if [ "$REM" -gt 0 ]; then
|
||||
dd if="/dev/r$SSDISK" bs=1048576 skip=$FULL count=1 2>/dev/null | dd bs=1 count=$REM >>"$SS" || fail "dd SharedSupport.dmg tail"
|
||||
fi
|
||||
[ "$(stat -f %z "$SS")" = "$PKG_BYTES" ] || fail "SharedSupport.dmg size mismatch: $(stat -f %z "$SS") != $PKG_BYTES"
|
||||
tail -c 512 "$SS" | grep -qa koly || fail "SharedSupport.dmg has no UDIF koly footer"
|
||||
chflags -h norestricted "$SS" 2>/dev/null || true
|
||||
echo "vmix-install: app=$APP SharedSupport.dmg=$(stat -f %z "$SS") bytes"
|
||||
prepare_target() {
|
||||
diskutil eraseDisk APFS "$VOLUME_NAME" GPT "$TARGET" || fail "eraseDisk"
|
||||
[ -d "$VOL" ] || fail "$VOL not mounted after erase"
|
||||
tar -xf "$V/installer-app.tar" -C "$VOL" || fail "untar installer app"
|
||||
[ -x "$APP/Contents/Resources/startosinstall" ] || fail "startosinstall missing from $APP"
|
||||
mkdir -p "$APP/Contents/SharedSupport"
|
||||
FULL=$(( PKG_BYTES / 1048576 )); REM=$(( PKG_BYTES % 1048576 ))
|
||||
echo "VMIX-INSTALL: copying SharedSupport.dmg ($PKG_BYTES bytes) from /dev/r$SSDISK"
|
||||
dd if="/dev/r$SSDISK" of="$SS" bs=1048576 count=$FULL || fail "dd SharedSupport"
|
||||
[ "$REM" -gt 0 ] && { dd if="/dev/r$SSDISK" bs=1048576 skip=$FULL count=1 | dd bs=1 count=$REM >> "$SS"; } || true
|
||||
[ "$(stat -f %z "$SS")" = "$PKG_BYTES" ] || fail "SharedSupport.dmg size $(stat -f %z "$SS") != $PKG_BYTES"
|
||||
tail -c 512 "$SS" | grep -qa koly || fail "SharedSupport.dmg has no koly footer"
|
||||
chflags -h norestricted "$SS" 2>/dev/null || true
|
||||
sync
|
||||
}
|
||||
prepare_target
|
||||
SOI="$APP/Contents/Resources/startosinstall"
|
||||
echo "VMIX-INSTALL: app ready, clock $(date -u)"
|
||||
|
||||
# macOS certificate validation needs a sane clock; a fresh VM RTC can be wrong.
|
||||
echo "vmix-install: guest clock is $(date) (UTC $(date -u))"
|
||||
if [ -n "${BUILD_DATE:-}" ]; then
|
||||
date -u "$BUILD_DATE" && echo "vmix-install: set clock to $(date)"
|
||||
fi
|
||||
|
||||
# Blackhole Apple's install/verify endpoints so osinstallersetupd's network calls
|
||||
# fail immediately instead of timing out (prepare otherwise crawls). Fully offline.
|
||||
# Offline install: no NIC is attached. Blackhole Apple's install/verify endpoints
|
||||
# too, so osinstallersetupd's requests fail immediately instead of timing out.
|
||||
for d in swscan.apple.com swcdn.apple.com swdist.apple.com swquery.apple.com \
|
||||
gs.apple.com gsa.apple.com gdmf.apple.com mesu.apple.com xp.apple.com \
|
||||
albert.apple.com captive.apple.com deviceservices-external.apple.com \
|
||||
|
|
@ -90,50 +67,34 @@ for d in swscan.apple.com swcdn.apple.com swdist.apple.com swquery.apple.com \
|
|||
ocsp2.apple.com valid.apple.com; do
|
||||
echo "127.0.0.1 $d" >> /etc/hosts
|
||||
done
|
||||
echo "vmix-install: blackholed Apple install endpoints for a fast offline prepare"
|
||||
|
||||
# --- 3. unattended install. startosinstall prepares then reboots the machine
|
||||
# itself into the install phase. Prepare intermittently stalls (~46% — an online
|
||||
# verify/personalization step through the VM's NAT), so a watchdog kills and
|
||||
# retries startosinstall if the target volume makes no write progress for a while.
|
||||
# The vmix agent pkg installs during the install phase and runs on first boot.
|
||||
# quote args properly — $VOL contains a space ("Macintosh HD")
|
||||
# --- 3. startosinstall prepares (~5 min) then reboots the machine itself into the
|
||||
# install phase; it never returns on success. Prepare is intermittently slow in
|
||||
# QEMU, so an attempt that stalls or runs too long is killed and retried on a
|
||||
# freshly erased target.
|
||||
run_soi() { "$SOI" --volume "$VOL" --agreetolicense --nointeraction --rebootdelay 5 "$@"; }
|
||||
free_kb() { df -k "$VOL" 2>/dev/null | awk 'NR==2 {print $4}'; }
|
||||
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 10 ]; do
|
||||
attempt=$((attempt + 1))
|
||||
echo "vmix-install: startosinstall attempt $attempt"
|
||||
if [ "$attempt" -eq 1 ]; then
|
||||
run_soi --installpackage "$V/vmix-agent.pkg" 2>&1 &
|
||||
else
|
||||
# a stalled attempt leaves the volume dirty; re-erase and rebuild for a clean retry
|
||||
diskutil eraseDisk APFS "$VOLUME_NAME" GPT "$TARGET" || fail "eraseDisk on retry"
|
||||
tar -xf "$V/installer-app.tar" -C "$VOL" || fail "untar on retry"
|
||||
mkdir -p "$APP/Contents/SharedSupport"
|
||||
dd if="/dev/r$SSDISK" of="$SS" bs=1048576 count=$FULL 2>/dev/null
|
||||
[ "$REM" -gt 0 ] && dd if="/dev/r$SSDISK" bs=1048576 skip=$FULL count=1 2>/dev/null | dd bs=1 count=$REM >>"$SS" 2>/dev/null
|
||||
chflags -h norestricted "$SS" 2>/dev/null || true
|
||||
run_soi --installpackage "$V/vmix-agent.pkg" 2>&1 &
|
||||
fi
|
||||
try=0
|
||||
while [ "$try" -lt 6 ]; do
|
||||
try=$((try + 1))
|
||||
[ "$try" -gt 1 ] && prepare_target
|
||||
echo "VMIX-INSTALL: startosinstall try $try"
|
||||
run_soi 2>&1 &
|
||||
SOI_PID=$!
|
||||
# watchdog: kill startosinstall if free space stalls for ~4 min OR the attempt
|
||||
# simply takes too long (prepare is intermittently slow; healthy = a few minutes)
|
||||
last=$(free_kb); stalled=0; elapsed=0
|
||||
while kill -0 "$SOI_PID" 2>/dev/null; do
|
||||
sleep 30; elapsed=$((elapsed + 30))
|
||||
now=$(free_kb)
|
||||
if [ "$now" = "$last" ]; then stalled=$((stalled + 30)); else stalled=0; last=$now; fi
|
||||
if [ "$stalled" -ge 240 ] || [ "$elapsed" -ge 540 ]; then
|
||||
echo "vmix-install: prepare too slow (stalled=${stalled}s elapsed=${elapsed}s), killing to retry"
|
||||
[ $((elapsed % 120)) -eq 0 ] && echo "VMIX-INSTALL: prepare running ${elapsed}s (stalled ${stalled}s)"
|
||||
if [ "$stalled" -ge 240 ] || [ "$elapsed" -ge 600 ]; then
|
||||
echo "VMIX-INSTALL: prepare too slow (stalled=${stalled}s elapsed=${elapsed}s), killing to retry"
|
||||
kill -9 "$SOI_PID" 2>/dev/null; pkill -9 -f startosinstall 2>/dev/null
|
||||
break
|
||||
fi
|
||||
done
|
||||
wait "$SOI_PID" 2>/dev/null
|
||||
# on success startosinstall reboots the machine and we never get here
|
||||
echo "vmix-install: startosinstall attempt $attempt ended without rebooting"
|
||||
echo "VMIX-INSTALL: startosinstall try $try ended without rebooting"
|
||||
sleep 3
|
||||
done
|
||||
fail "startosinstall did not complete after $attempt attempts"
|
||||
fail "startosinstall did not complete after $try tries"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue