diff --git a/dstack/supervisor/src/process.rs b/dstack/supervisor/src/process.rs index c424f9b51..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,8 +300,13 @@ impl Process { if is_running { bail!("Missing kill tx for process"); } + state.normalize_clean_exit(); return Ok(()); }; + if !is_running { + state.normalize_clean_exit(); + 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..9b3dcb22e 100644 --- a/dstack/vmm/src/main_service.rs +++ b/dstack/vmm/src/main_service.rs @@ -810,8 +810,16 @@ 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 + .supervisor + .info(&request.id) + .await? + .context("Supervisor process not found")?; + self.app.stop_vm_process(&request.id).await } async fn sv_remove(self, request: Id) -> Result<()> {