From 67ded34a3786ce6db00d371b8f294b5663cf8995 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 15:35:57 +0000 Subject: [PATCH 1/3] fix(vmm): reap VM launcher children on SvStop --- dstack/supervisor/src/process.rs | 11 +++++++++++ dstack/vmm/src/app.rs | 2 +- dstack/vmm/src/main_service.rs | 7 +++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/dstack/supervisor/src/process.rs b/dstack/supervisor/src/process.rs index c424f9b51..5212034e0 100644 --- a/dstack/supervisor/src/process.rs +++ b/dstack/supervisor/src/process.rs @@ -289,8 +289,19 @@ impl Process { if is_running { bail!("Missing kill tx for process"); } + state.status = ProcessStatus::Stopped; + state.stopped_at = Some(SystemTime::now()); return Ok(()); }; + if !is_running { + // A process may exit cleanly just before an explicit stop reaches + // Supervisor (VM launchers do this after reaping their children). + // The requested persistent state is nevertheless stopped, not a + // stale natural-exit observation. + state.status = ProcessStatus::Stopped; + state.stopped_at = Some(SystemTime::now()); + return Ok(()); + } match stop_tx.send(()) { Ok(()) => Ok(()), Err(()) => match is_running { diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index 690b7c7af..424a18edb 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -468,7 +468,7 @@ impl App { Ok(()) } - async fn stop_vm_process(&self, id: &str) -> Result<()> { + pub(crate) async fn stop_vm_process(&self, id: &str) -> Result<()> { let Some(info) = self.supervisor.info(id).await? else { return Ok(()); }; diff --git a/dstack/vmm/src/main_service.rs b/dstack/vmm/src/main_service.rs index 0c8c97948..cb455ffd7 100644 --- a/dstack/vmm/src/main_service.rs +++ b/dstack/vmm/src/main_service.rs @@ -810,8 +810,11 @@ impl VmmRpc for RpcHandler { } async fn sv_stop(self, request: Id) -> Result<()> { - self.app.supervisor.stop(&request.id).await?; - Ok(()) + // VM launcher processes own QEMU and swtpm children. Route them through + // the VM-aware stop path so the launcher can reap those children; the + // same helper preserves generic Supervisor stop semantics for every + // other process type. + self.app.stop_vm_process(&request.id).await } async fn sv_remove(self, request: Id) -> Result<()> { From 845096a16eb0d9244df101eac76cb1bffc0a6c35 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 15:38:59 +0000 Subject: [PATCH 2/3] fix(vmm): keep SvStop unknown IDs rejected --- dstack/vmm/src/main_service.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dstack/vmm/src/main_service.rs b/dstack/vmm/src/main_service.rs index cb455ffd7..9b3dcb22e 100644 --- a/dstack/vmm/src/main_service.rs +++ b/dstack/vmm/src/main_service.rs @@ -814,6 +814,11 @@ impl VmmRpc for RpcHandler { // the VM-aware stop path so the launcher can reap those children; the // same helper preserves generic Supervisor stop semantics for every // other process type. + self.app + .supervisor + .info(&request.id) + .await? + .context("Supervisor process not found")?; self.app.stop_vm_process(&request.id).await } From 09fd2aa59f7ce32930cb7711788bd18e27b6b4d4 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 3 Aug 2026 07:14:41 -0700 Subject: [PATCH 3/3] fix(supervisor): only normalize clean exits to stopped on explicit stop Overwriting Exited(code)/Error states with Stopped on SvStop masked crash diagnostics in sv_list and clobbered the exit timestamp the wait task had recorded. Narrow the normalization to Exited(0) so an explicit stop still reads as stopped while non-zero exit codes and error states stay visible. --- dstack/supervisor/src/process.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/dstack/supervisor/src/process.rs b/dstack/supervisor/src/process.rs index 5212034e0..94e0c61e5 100644 --- a/dstack/supervisor/src/process.rs +++ b/dstack/supervisor/src/process.rs @@ -83,6 +83,17 @@ impl ProcessStateRT { pub(crate) fn is_started(&self) -> bool { self.started } + + /// An explicit stop may arrive just after the process exited on its own + /// (VM launchers exit cleanly after reaping their children). Report such + /// a clean exit as the intended stop, but keep non-zero exit codes and + /// errors visible for diagnostics. `stopped_at` recorded by the wait task + /// is left untouched. + fn normalize_clean_exit(&mut self) { + if matches!(self.status, ProcessStatus::Exited(0)) { + self.status = ProcessStatus::Stopped; + } + } } impl ProcessStateRT { @@ -289,17 +300,11 @@ impl Process { if is_running { bail!("Missing kill tx for process"); } - state.status = ProcessStatus::Stopped; - state.stopped_at = Some(SystemTime::now()); + state.normalize_clean_exit(); return Ok(()); }; if !is_running { - // A process may exit cleanly just before an explicit stop reaches - // Supervisor (VM launchers do this after reaping their children). - // The requested persistent state is nevertheless stopped, not a - // stale natural-exit observation. - state.status = ProcessStatus::Stopped; - state.stopped_at = Some(SystemTime::now()); + state.normalize_clean_exit(); return Ok(()); } match stop_tx.send(()) {