internal/supervise/supervise.go holds s.mu across Engine.Running, and nothing on that path has a deadline.
Path: tick → engineAdapter.Running → Provisioner.engineRunning → Fast.List → comSession.do(ctx). The ctx is the supervisor's process-lifetime context — there is no per-tick timeout anywhere on it.
If EnumerateDistributions hangs — a service restart, wsl --update, a wedged VM — do blocks on <-done until shutdown while holding s.mu, and then:
- every
Demand() blocks, so every docker connection through demandDialer hangs rather than failing — a hang is worse than an error here, because the CLI just sits there
LifecycleSnapshot blocks, so the stats file freezes and the tray cannot even show that the supervisor is stuck
- the poke loop never runs again (same goroutine)
The COM rewrite amplified this
Before #380, COM callers were independent processes shelling out to wsl.exe. Now every caller — List, Terminate, doctor — queues behind one unbuffered channel and one OS thread (the apartment goroutine). A single hung call stalls callers that used to be unable to affect each other.
Related, same file: a panic on the apartment thread has no recover, so it takes the whole supervisor down — bridge and reconciler included. unsafe.Slice(arr, count) panics if the service ever returns count > 0 with a nil array, and hr == 0 is the only guard.
Fix
- A per-tick deadline on the engine probe, sized so a slow-but-alive
wsl call still succeeds. The supervisor's own ctx is the wrong bound for a single tick.
- Do not hold
s.mu across it. The probe's result is what needs the lock, not the probe.
recover on the apartment goroutine, and restart the loop rather than leaving it dead — a dead loop with s.stop unclosed turns every later do into a block until its ctx fires, which on this path is never.
Found in the pre-0.6.0 independent concurrency review.
internal/supervise/supervise.goholdss.muacrossEngine.Running, and nothing on that path has a deadline.Path:
tick→engineAdapter.Running→Provisioner.engineRunning→Fast.List→comSession.do(ctx). The ctx is the supervisor's process-lifetime context — there is no per-tick timeout anywhere on it.If
EnumerateDistributionshangs — a service restart,wsl --update, a wedged VM —doblocks on<-doneuntil shutdown while holdings.mu, and then:Demand()blocks, so every docker connection throughdemandDialerhangs rather than failing — a hang is worse than an error here, because the CLI just sits thereLifecycleSnapshotblocks, so the stats file freezes and the tray cannot even show that the supervisor is stuckThe COM rewrite amplified this
Before #380, COM callers were independent processes shelling out to
wsl.exe. Now every caller —List,Terminate, doctor — queues behind one unbuffered channel and one OS thread (the apartment goroutine). A single hung call stalls callers that used to be unable to affect each other.Related, same file: a panic on the apartment thread has no
recover, so it takes the whole supervisor down — bridge and reconciler included.unsafe.Slice(arr, count)panics if the service ever returnscount > 0with a nil array, andhr == 0is the only guard.Fix
wslcall still succeeds. The supervisor's own ctx is the wrong bound for a single tick.s.muacross it. The probe's result is what needs the lock, not the probe.recoveron the apartment goroutine, and restart the loop rather than leaving it dead — a dead loop withs.stopunclosed turns every laterdointo a block until its ctx fires, which on this path is never.Found in the pre-0.6.0 independent concurrency review.