From 7093d351248de699dbca2ccc38b9bec51feb3351 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:46:40 +0800 Subject: [PATCH] fix(run): stop runs and terminals when the app exits Closing a window stops what its project started, but a quit that doesn't close the windows first (the Dock's Quit, the app menu, a logout) exited with the runs still alive: a server kept its port and the next run of the configuration failed with "port already in use". On exit the app now kills every run's process tree and every terminal shell, as the remote server already does when it shuts down. Fixes #1691 --- crates/ide-host/src/lib.rs | 8 ++++++++ crates/ide-run/tests/process.rs | 24 ++++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + 3 files changed, 33 insertions(+) diff --git a/crates/ide-host/src/lib.rs b/crates/ide-host/src/lib.rs index c1c76fdb..2dad9a5a 100644 --- a/crates/ide-host/src/lib.rs +++ b/crates/ide-host/src/lib.rs @@ -349,6 +349,14 @@ impl Host { } } + /// Called on desktop exit: kills the runs and terminal shells still alive. Closing a + /// window stops what it started, but a quit from the Dock, the app menu or a logout + /// exits without closing the windows, and the process trees would outlive the app. + pub fn stop_processes_on_exit(&self) { + self.processes.kill_all(); + self.terminals.kill_all(); + } + /// Called before desktop exit, which may bypass document destructors. pub fn clear_workspace_edit_history_on_exit(&self) { for project in self.projects() { diff --git a/crates/ide-run/tests/process.rs b/crates/ide-run/tests/process.rs index cbb2ba91..21524e4f 100644 --- a/crates/ide-run/tests/process.rs +++ b/crates/ide-run/tests/process.rs @@ -529,6 +529,30 @@ fn revoking_one_project_stops_only_its_runs() { h.pm.stop(theirs.id).unwrap(); } +/// Quitting the app kills what every project's runs started, forked children included (#1691). +#[test] +fn kill_all_stops_every_projects_process_trees() { + let h = harness(); + let other = tempfile::tempdir().unwrap(); + let (mine, chunks) = start(&h, &cfg("mine", "/bin/sh", &["-c", "sleep 30 & echo $!; wait"])); + let grandchild: i32 = wait_for(&chunks, "\n").trim().parse().expect("pid"); + let theirs = + h.pm.start( + other.path(), + &cfg("theirs", "/bin/sh", &["-c", "sleep 30"]), + &StartOptions::default(), + h.sink.clone(), + Box::new(|_| {}), + ) + .unwrap(); + h.pm.kill_all(); + let deadline = Instant::now() + Duration::from_secs(8); + while alive(grandchild) || h.pm.list().iter().any(|r| (r.id == mine || r.id == theirs.id) && r.running) { + assert!(Instant::now() < deadline, "a run survived kill_all"); + std::thread::sleep(Duration::from_millis(50)); + } +} + #[test] #[ignore = "requires CMake, CTest and a C++ compiler"] fn cmake_configure_discovers_builds_and_runs_a_multifile_target() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 86dc6dc2..9e02a874 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -234,6 +234,7 @@ pub fn run() { let _ = &early_opens; if let tauri::RunEvent::Exit = event { if let Some(state) = app.try_state::() { + state.host.stop_processes_on_exit(); state.host.clear_workspace_edit_history_on_exit(); state.host.shutdown_notebooks_on_exit(); }