Skip to content

Commit b1b48ca

Browse files
committed
fix(vm): fix start-state detection, and eliminate clone race in vm:reset, auto-reload UTM
Signed-off-by: Rado M <radkomih@gmail.com>
1 parent a485ff8 commit b1b48ca

1 file changed

Lines changed: 106 additions & 18 deletions

File tree

taskfiles/vm.yaml

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,28 @@ tasks:
115115
echo "VM Golden Name: {{.VM_GOLDEN_NAME}}"
116116
echo "UTM VMs Directory: {{.UTM_VMS_DIR}}"
117117
echo 'Force Download: {{ eq .FORCE "true" }}'
118-
118+
119+
# Quit and relaunch UTM so it reloads its VM registry from disk. UTM does
120+
# not live-watch its Documents dir, so VM bundles copied in underneath a
121+
# running UTM stay invisible until it is bounced.
122+
reload_utm() {
123+
echo "Reloading UTM so it picks up VM changes on disk..."
124+
osascript -e 'tell application "UTM" to quit' >/dev/null 2>&1 || true
125+
sleep 3
126+
pkill -f "UTM.app/Contents/MacOS/UTM" >/dev/null 2>&1 || true
127+
sleep 2
128+
open -g -a UTM || true
129+
# Wait until UTM is scriptable again before relying on utmctl.
130+
for _ in $(seq 1 10); do
131+
if osascript -e 'with timeout of 5 seconds' \
132+
-e 'tell application "UTM" to get name of every virtual machine' \
133+
-e 'end timeout' >/dev/null 2>&1; then
134+
break
135+
fi
136+
sleep 2
137+
done
138+
}
139+
119140
if [ "{{.FORCE}}" = "true" ]; then
120141
echo "Force flag is set, re-downloading golden VM image..."
121142
else
@@ -128,9 +149,15 @@ tasks:
128149
# if the .utm file exists in UTM_VMS_DIR, skip download
129150
echo "Checking UTM VMs directory for existing golden VM...: {{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm"
130151
if [ -d "{{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm" ] || [ -f "{{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm" ]; then
131-
echo "✓ Golden VM already exists in UTM directory: {{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm"
132-
echo "👉 Please restart UTM to reload VMs ('open -a UTM'), then run 'task vm:start'."
133-
exit 1 # non zero would stop further steps
152+
echo "✓ Golden VM exists on disk but UTM hasn't loaded it: {{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm"
153+
reload_utm
154+
if utmctl status "{{.VM_GOLDEN_NAME}}" >/dev/null 2>&1; then
155+
echo "✓ UTM reloaded and now sees the golden VM."
156+
exit 0
157+
fi
158+
echo "❌ UTM still doesn't see {{.VM_GOLDEN_NAME}} after a reload."
159+
echo " Open UTM and import it manually, then re-run."
160+
exit 1
134161
else
135162
echo "✓ No existing golden VM found, proceeding to download."
136163
fi
@@ -193,10 +220,18 @@ tasks:
193220
194221
# Copy the VM to UTM directory and rename
195222
cp -r "$TEMP_DIR/golden-image.utm" "{{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm"
196-
197-
echo "✓ Golden VM downloaded successfully and placed in UTM directory: {{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm"
198-
echo "👉 Please restart UTM to reload VMs ('open -a UTM'), then run 'task vm:start'."
199-
exit 1 # non zero would stop further steps
223+
echo "✓ Golden VM placed in UTM directory: {{.UTM_VMS_DIR}}/{{.VM_GOLDEN_NAME}}.utm"
224+
225+
# We just wrote a VM bundle under a running UTM; reload it so it registers
226+
# the new golden VM, then let the caller (vm:reset) continue.
227+
reload_utm
228+
if utmctl status "{{.VM_GOLDEN_NAME}}" >/dev/null 2>&1; then
229+
echo "✓ UTM reloaded and now sees the golden VM."
230+
exit 0
231+
fi
232+
echo "❌ UTM still doesn't see {{.VM_GOLDEN_NAME}} after a reload."
233+
echo " Open UTM and import it manually, then re-run."
234+
exit 1
200235
201236
vm:reset:
202237
desc: "Clone the golden VM to create a working instance with passwordless SSH"
@@ -231,15 +266,27 @@ tasks:
231266
if utmctl status "{{.VM_NAME}}" >/dev/null 2>&1; then
232267
echo "Stopping and removing existing working VM..."
233268
utmctl stop "{{.VM_NAME}}" 2>/dev/null || true
234-
sleep 2
235269
utmctl delete "{{.VM_NAME}}" 2>/dev/null || true
236-
sleep 1
270+
# Wait until UTM drops the VM from its registry (status goes non-zero =
271+
# not found) so the clone below doesn't race a half-deleted entry.
272+
for _ in $(seq 1 10); do
273+
utmctl status "{{.VM_NAME}}" >/dev/null 2>&1 || break
274+
sleep 1
275+
done
237276
fi
238-
277+
239278
# Clone the golden VM
240279
echo "Cloning '{{.VM_GOLDEN_NAME}}' to '{{.VM_NAME}}'..."
241280
utmctl clone "{{.VM_GOLDEN_NAME}}" --name "{{.VM_NAME}}"
242-
281+
282+
# Wait until UTM has registered the clone as a stopped VM before starting it.
283+
# Starting immediately after a clone can wedge UTM's Apple Event handler.
284+
echo "Waiting for UTM to register the cloned VM..."
285+
for _ in $(seq 1 15); do
286+
[ "$(utmctl status "{{.VM_NAME}}" 2>/dev/null)" = "stopped" ] && break
287+
sleep 1
288+
done
289+
243290
echo "✓ VM cloned. Now starting it for initial configuration..."
244291
- task: vm:start
245292
- task: vm:configure:ssh
@@ -265,19 +312,27 @@ tasks:
265312
deps:
266313
- vm:ensure-exists
267314
cmds:
268-
- utmctl start --hide "{{.VM_NAME}}"
269315
- |
270-
echo "Waiting for VM to boot (checking every 5 seconds)..."
271-
316+
echo "Starting VM and waiting for it to boot (checking every 5 seconds)..."
317+
272318
# Poll for VM to be ready with timeout
273319
MAX_ATTEMPTS=24 # 24 * 5 seconds = 2 minutes max wait
274320
ATTEMPT=0
321+
# utmctl start can exit 0 yet not launch the VM, so we verify run state
322+
# and re-issue the start while still stopped, up to this many times.
323+
MAX_START_ATTEMPTS=3
324+
START_ATTEMPTS=0
325+
326+
utmctl start --hide "{{.VM_NAME}}" 2>&1 || true
327+
START_ATTEMPTS=1
275328
276329
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
277330
ATTEMPT=$((ATTEMPT + 1))
278331
279-
# Check if VM is running
280-
if utmctl status "{{.VM_NAME}}" >/dev/null 2>&1; then
332+
# Gate on the reported state: utmctl status exits 0 even when stopped
333+
# (only "not found" is non-zero), so the exit code can't gate this.
334+
STATE=$(utmctl status "{{.VM_NAME}}" 2>/dev/null)
335+
if [ "$STATE" = "started" ]; then
281336
echo "✓ VM is running, checking if network is ready..."
282337
283338
VM_IP=$({{.GET_VM_IP}} get_vm_ip)
@@ -287,8 +342,41 @@ tasks:
287342
else
288343
echo "VM is running but network not ready yet (attempt $ATTEMPT/$MAX_ATTEMPTS)..."
289344
fi
345+
elif [ "$STATE" = "stopped" ]; then
346+
# Start didn't take. A plain retry clears a transient glitch; the final
347+
# attempt relaunches UTM.app, which clears a wedged start path.
348+
if [ $START_ATTEMPTS -lt $MAX_START_ATTEMPTS ]; then
349+
START_ATTEMPTS=$((START_ATTEMPTS + 1))
350+
if [ $START_ATTEMPTS -eq $MAX_START_ATTEMPTS ]; then
351+
# Bounce UTM.app; safe since the target VM is stopped.
352+
echo "VM still stopped; relaunching UTM.app to clear a stuck start path..."
353+
osascript -e 'tell application "UTM" to quit' >/dev/null 2>&1 || true
354+
sleep 3
355+
pkill -f "UTM.app/Contents/MacOS/UTM" >/dev/null 2>&1 || true
356+
sleep 2
357+
open -g -a UTM || true
358+
# Wait until UTM is scriptable again before re-issuing the start.
359+
for _ in $(seq 1 10); do
360+
if osascript -e 'with timeout of 5 seconds' \
361+
-e 'tell application "UTM" to get name of every virtual machine' \
362+
-e 'end timeout' >/dev/null 2>&1; then
363+
break
364+
fi
365+
sleep 2
366+
done
367+
else
368+
echo "VM still stopped; re-issuing start (try $START_ATTEMPTS/$MAX_START_ATTEMPTS)..."
369+
fi
370+
utmctl start --hide "{{.VM_NAME}}" 2>&1 || true
371+
else
372+
echo "❌ VM is still 'stopped' after $MAX_START_ATTEMPTS start attempts (including a UTM relaunch)."
373+
echo " Try manually: quit UTM (⌘Q), reopen it, then 'task vm:start'."
374+
echo " If it still fails, open the VM in the UTM GUI and start it there"
375+
echo " to surface any blocking dialog or boot error. Aborting."
376+
exit 1
377+
fi
290378
else
291-
echo "VM is starting... (attempt $ATTEMPT/$MAX_ATTEMPTS)"
379+
echo "VM is starting (state: ${STATE:-unknown}, attempt $ATTEMPT/$MAX_ATTEMPTS)..."
292380
fi
293381
294382
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then

0 commit comments

Comments
 (0)