Compare commits

...
Author SHA1 Message Date
l0ng-ai 839e5d72fd fix(project): open a window's project once when opens race
Two opens of one window could each find no current project and build a full project (index, language servers, watchers); the second replaced the first without closing it, leaving two jdtls on one workspace. open_project and open_light now hold a per-window lock from the check to the insert.
2026-11-05 16:05:39 +08:00
3 changed files with 60 additions and 0 deletions

No files matched your search

+10
View File
@@ -86,6 +86,8 @@ pub struct Host {
project_requests: RwLock<HashMap<String, Vec<RequestId>>>,
/// Shutdowns of projects closed with [`Host::close_project_detached`].
closing: parking_lot::Mutex<Vec<std::thread::JoinHandle<()>>>,
/// One open at a time per window (see [`Host::opening`]).
open_locks: parking_lot::Mutex<HashMap<String, Arc<parking_lot::Mutex<()>>>>,
}
/// How long exit waits for project shutdown before giving up.
@@ -135,6 +137,7 @@ impl Host {
requests: Default::default(),
project_requests: RwLock::default(),
closing: Default::default(),
open_locks: Default::default(),
};
host.apply_proxy();
host
@@ -401,6 +404,13 @@ impl Host {
self.requests.cancel(id);
}
/// The lock an open of `window` holds from its check for the current project to its
/// insert: two opens racing (a double click, a launch path and a restore) would each start
/// an index, language servers and watchers, and the second would replace the first unclosed.
pub(crate) fn opening(&self, window: &str) -> Arc<parking_lot::Mutex<()>> {
self.open_locks.lock().entry(window.to_owned()).or_default().clone()
}
pub(crate) fn insert_project(&self, window: &str, project: Arc<Project>) {
self.projects.write().insert(window.to_owned(), project);
}
+4
View File
@@ -77,6 +77,8 @@ impl Host {
lap = std::time::Instant::now();
};
let root = dunce::canonicalize(root)?;
let opening = self.opening(window);
let _opening = opening.lock();
// Reject a damaged manifest before replacing the currently open project.
ide_workspace::workspace::Workspace::load(&root)?;
// The window reloaded (or asked again) for the project it already has: keep
@@ -569,6 +571,8 @@ impl Host {
return Err(IdeError::invalid(format!("{} is a folder", file.display())));
}
let dir = file.parent().map(Path::to_path_buf).ok_or_else(|| IdeError::invalid("no parent folder"))?;
let opening = self.opening(window);
let _opening = opening.lock();
if let Ok(current) = self.project(window)
&& current.light
{
+46
View File
@@ -0,0 +1,46 @@
//! Opening the project a window is already opening reuses it: racing opens (a double
//! click, a launch path and a restore) must not start a second index and language servers.
use ide_core::NullSink;
use ide_host::{Host, HostConfig};
use std::sync::{Arc, Barrier};
#[test]
fn racing_opens_of_one_window_share_one_project() {
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap().join("project");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/Main.java"), "class Main {}\n").unwrap();
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap();
let _guard = runtime.enter();
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: runtime.handle().clone(),
project_environment: false,
}));
host.trust.decide(&root, true, false).unwrap();
let start = Arc::new(Barrier::new(4));
let opens: Vec<_> = (0..4)
.map(|_| {
let (host, root, start) = (host.clone(), root.clone(), start.clone());
std::thread::spawn(move || {
start.wait();
host.open_project("main", &root).unwrap()
})
})
.collect();
let projects: Vec<_> = opens.into_iter().map(|t| t.join().unwrap()).collect();
let current = host.project("main").unwrap();
for project in &projects {
assert!(Arc::ptr_eq(project, &current), "every open returns the window's one project");
}
host.close_project("main");
}