mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix(host): close a project's background processes with it (#852)
Closing a project only cancelled its agents and notebooks; its window's requests, run processes, terminal shells and debug sessions kept running until app exit, and the language server shutdown was a detached task a closing app could drop (orphaned jdtls). Closing now cancels the window's requests, kills its run processes and shells, stops its debug sessions and waits for the language servers to shut down; other windows are untouched. Terminals are keyed by owner window so they can be closed per window. Co-authored-by: misswell <[email protected]>
This commit is contained in:
1 parent
362ae3535c
commit
78b23df7b5
7 files changed
+104
-7
No files matched your search
@@ -1389,6 +1389,16 @@ impl DebugManager {
|
||||
})
|
||||
}
|
||||
|
||||
/// Stops every session `key` started: the window that opened the project is closing.
|
||||
pub async fn stop_key(&self, key: &str) {
|
||||
let ids: Vec<DebugSessionId> = self.sessions(Some(key)).into_iter().map(|s| s.id).collect();
|
||||
for id in ids {
|
||||
if let Err(e) = self.stop(id).await {
|
||||
tracing::debug!("debug session {id:?} did not stop: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops a session and its child sessions.
|
||||
pub async fn stop(&self, s: DebugSessionId) -> IdeResult<()> {
|
||||
let session = self.session(s)?;
|
||||
|
||||
@@ -176,12 +176,21 @@ impl Host {
|
||||
}
|
||||
project.agents.shutdown();
|
||||
project.notebooks.stop_admission();
|
||||
// What the closing window started goes with it — the builds and searches it
|
||||
// asked for, its runs, its shells and its debug sessions — and the language
|
||||
// servers finish shutting down before the window is gone, as IDEA closes a
|
||||
// project's processes.
|
||||
for request in self.project_requests.write().remove(self.owner_window(window)).unwrap_or_default() {
|
||||
self.cancel_request(request);
|
||||
}
|
||||
self.processes.kill_project(&project.root);
|
||||
self.terminals.kill_key(self.owner_window(window));
|
||||
self.shutdown_project_workers_blocking(
|
||||
vec![project.notebooks.clone()],
|
||||
project.kotlin_bridge.iter().cloned().collect(),
|
||||
Some(project.lsp.clone()),
|
||||
Some((self.debugger.clone(), window.to_owned())),
|
||||
);
|
||||
let lsp = project.lsp.clone();
|
||||
self.runtime.spawn(async move { lsp.shutdown_all().await });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +200,8 @@ impl Host {
|
||||
&self,
|
||||
notebooks: Vec<Arc<notebook::NotebookService>>,
|
||||
bridges: Vec<Arc<kotlin_bridge::KotlinBridge>>,
|
||||
lsp: Option<Arc<ide_lsp::LspManager>>,
|
||||
debug: Option<(Arc<ide_dap::DebugManager>, String)>,
|
||||
) {
|
||||
for bridge in &bridges {
|
||||
bridge.stop();
|
||||
@@ -208,6 +219,12 @@ impl Host {
|
||||
for service in notebooks {
|
||||
service.shutdown().await;
|
||||
}
|
||||
if let Some(lsp) = lsp {
|
||||
lsp.shutdown_all().await;
|
||||
}
|
||||
if let Some((debug, key)) = debug {
|
||||
debug.stop_key(&key).await;
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Err(error) = worker.join() {
|
||||
@@ -229,6 +246,8 @@ impl Host {
|
||||
self.shutdown_project_workers_blocking(
|
||||
projects.iter().map(|project| project.notebooks.clone()).collect(),
|
||||
projects.iter().filter_map(|project| project.kotlin_bridge.clone()).collect(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ pub fn spawn(
|
||||
.ok()
|
||||
.filter(|p| p.is_trusted())
|
||||
.and_then(|p| ide_sdk::apply::terminal_activation(&p.root, &p.settings.effective(), &shell));
|
||||
let info = host.terminals.spawn(options, host.window_sink(window), Box::new(move |chunk| on_output(chunk)))?;
|
||||
let key = host.owner_window(window).to_owned();
|
||||
let info = host.terminals.spawn(options, key, host.window_sink(window), Box::new(move |chunk| on_output(chunk)))?;
|
||||
if let Some(line) = activation {
|
||||
let _ = host.terminals.write(info.id, &format!(" {line}\r"));
|
||||
}
|
||||
|
||||
@@ -135,9 +135,7 @@ impl crate::Host {
|
||||
for request in requests {
|
||||
self.cancel_request(request);
|
||||
}
|
||||
for session in self.debugger.sessions(Some(&window)) {
|
||||
let _ = self.debugger.stop(session.id).await;
|
||||
}
|
||||
self.debugger.stop_key(&window).await;
|
||||
}
|
||||
}
|
||||
project.lsp.reconfigure().await;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Closing a project takes the background processes of its window with it, as IDEA does,
|
||||
//! and leaves the other windows' processes alone.
|
||||
#![cfg(unix)]
|
||||
|
||||
use ide_core::NullSink;
|
||||
use ide_host::{Host, HostConfig, ops};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn options(root: &std::path::Path) -> ide_terminal::TerminalOptions {
|
||||
ide_terminal::TerminalOptions {
|
||||
cwd: root.to_path_buf().into(),
|
||||
// A fixed shell: the test must not depend on the login shell of this machine.
|
||||
shell: Some("/bin/sh".into()),
|
||||
args: vec![],
|
||||
env: Default::default(),
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
shell_integration: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_project_closes_its_terminals_and_keeps_other_windows() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(tmp.path()).unwrap().join("project");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap();
|
||||
runtime.block_on(async {
|
||||
let host = Arc::new(Host::new(HostConfig {
|
||||
config_dir: tmp.path().join("config"),
|
||||
data_dir: tmp.path().join("data"),
|
||||
local_data_dir: tmp.path().join("data"),
|
||||
languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()),
|
||||
syntax: Arc::default(),
|
||||
sink_for: Box::new(|_| Arc::new(NullSink)),
|
||||
owner: |w| w,
|
||||
runtime: tokio::runtime::Handle::current(),
|
||||
project_environment: false,
|
||||
}));
|
||||
host.trust.decide(&root, true, false).unwrap();
|
||||
host.open_project("main", &root).unwrap();
|
||||
let project_shell =
|
||||
ops::term::spawn(&host, "main", options(&root), Arc::new(|_| {})).expect("project terminal");
|
||||
let other_shell = ops::term::spawn(&host, "other", options(&root), Arc::new(|_| {})).expect("other terminal");
|
||||
assert_eq!(host.terminals.list().len(), 2);
|
||||
|
||||
host.close_project("main");
|
||||
|
||||
let left: Vec<_> = host.terminals.list().into_iter().map(|t| t.id).collect();
|
||||
assert!(!left.contains(&project_shell.id), "the closing window's shell is gone");
|
||||
assert!(left.contains(&other_shell.id), "another window keeps its shell");
|
||||
host.terminals.kill_key("other");
|
||||
});
|
||||
}
|
||||
@@ -35,6 +35,7 @@ async fn storage_requires_a_registered_terminal() {
|
||||
rows: 24,
|
||||
shell_integration: false,
|
||||
},
|
||||
"main".into(),
|
||||
Arc::new(NullSink),
|
||||
Box::new(|_| {}),
|
||||
)
|
||||
|
||||
@@ -53,6 +53,8 @@ pub struct TerminalInfo {
|
||||
}
|
||||
|
||||
struct Session {
|
||||
/// The window that opened it: closing that window's project closes its terminals.
|
||||
key: String,
|
||||
info: TerminalInfo,
|
||||
master: Mutex<Box<dyn MasterPty + Send>>,
|
||||
writer: Mutex<Box<dyn Write + Send>>,
|
||||
@@ -83,6 +85,7 @@ impl TerminalManager {
|
||||
pub fn spawn(
|
||||
&self,
|
||||
options: TerminalOptions,
|
||||
key: String,
|
||||
sink: SharedSink,
|
||||
on_output: Box<dyn Fn(String) + Send>,
|
||||
) -> IdeResult<TerminalInfo> {
|
||||
@@ -121,6 +124,7 @@ impl TerminalManager {
|
||||
let info = TerminalInfo { id, title: shell_title(&shell), pid: child.process_id() };
|
||||
let exited = Arc::new(AtomicBool::new(false));
|
||||
let session = Arc::new(Session {
|
||||
key,
|
||||
info: info.clone(),
|
||||
master: Mutex::new(pair.master),
|
||||
writer: Mutex::new(writer),
|
||||
@@ -160,6 +164,15 @@ impl TerminalManager {
|
||||
.map_err(|e| IdeError::internal(format!("resize failed: {e}")))
|
||||
}
|
||||
|
||||
/// Closes the terminals `key` opened: the project of that window is closing.
|
||||
pub fn kill_key(&self, key: &str) {
|
||||
let ids: Vec<TerminalId> =
|
||||
self.sessions.lock().iter().filter(|(_, s)| s.key == key).map(|(id, _)| *id).collect();
|
||||
for id in ids {
|
||||
let _ = self.kill(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kill(&self, id: TerminalId) -> IdeResult<()> {
|
||||
let session = self.sessions.lock().remove(&id).ok_or_else(|| not_found(id))?;
|
||||
terminate(&session);
|
||||
@@ -636,6 +649,7 @@ mod tests {
|
||||
.manager
|
||||
.spawn(
|
||||
opts,
|
||||
"test".into(),
|
||||
h.sink.clone(),
|
||||
Box::new(move |s| {
|
||||
let _ = tx.send(s);
|
||||
@@ -793,7 +807,7 @@ mod tests {
|
||||
let h = harness();
|
||||
let (other_sink, other_events) = channel_sink();
|
||||
let (mine, _rx) = spawn_collect(&h, sh("exit 1"));
|
||||
let theirs = h.manager.spawn(sh("exit 2"), other_sink, Box::new(|_| {})).expect("spawn");
|
||||
let theirs = h.manager.spawn(sh("exit 2"), "other".into(), other_sink, Box::new(|_| {})).expect("spawn");
|
||||
match h.events.recv_timeout(Duration::from_secs(10)).expect("exit event") {
|
||||
IdeEvent::TerminalExited { terminal, code } => assert_eq!((terminal, code), (mine.id, Some(1))),
|
||||
e => panic!("unexpected {e:?}"),
|
||||
|
||||
Reference in new issue
Block a user