diff --git a/src/app/mod.rs b/src/app/mod.rs index 6c9f5b38..93ab53c5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -406,6 +406,7 @@ impl App { crate::plugin_installations::retain_startup( &mut plugin_installation_leases, &restored_cwds, + false, )?; } let session_writer = Arc::new(std::sync::Mutex::new(crate::persist::SessionWriter::new( @@ -689,6 +690,11 @@ impl App { api_rx, event_hub, )?; + crate::plugin_installations::retain_startup( + &mut app.plugin_installation_leases, + &[], + true, + )?; let (workspaces, terminals, runtimes) = crate::persist::restore_handoff( snapshot, config.advanced.scrollback_limit_bytes, diff --git a/src/plugin_installations.rs b/src/plugin_installations.rs index d060716c..544ed450 100644 --- a/src/plugin_installations.rs +++ b/src/plugin_installations.rs @@ -90,7 +90,11 @@ fn installations() -> io::Result> { Ok(result) } -pub(crate) fn retain_startup(leases: &mut Leases, restored_cwds: &[PathBuf]) -> io::Result<()> { +pub(crate) fn retain_startup( + leases: &mut Leases, + restored_cwds: &[PathBuf], + handoff: bool, +) -> io::Result<()> { crate::persist::plugin_registry::with_registry_lock(|| { for installation in installations()? { if leases.contains_key(&installation) { @@ -110,6 +114,9 @@ pub(crate) fn retain_startup(leases: &mut Leases, restored_cwds: &[PathBuf]) -> .unwrap_or_else(|_| cwd.clone()) .starts_with(installation.join("checkout")) }); + if !referenced && !handoff { + continue; + } match file.try_lock() { Ok(()) if !referenced => continue, Ok(()) => file.unlock()?, @@ -166,6 +173,13 @@ pub(crate) fn cleanup() -> io::Result<()> { // Windows cannot remove the open lease file. Registry + mutation // locks exclude new readers/installers while the handle is closed. drop(lease); + // Keep the marker if removing checkout files fails so a later + // cleanup can retry (for example, an open Windows build artifact). + match std::fs::remove_dir_all(installation.join("checkout")) { + Ok(()) => {} + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + Err(err) => return Err(err), + } std::fs::remove_dir_all(&installation)?; } Ok(()) @@ -296,6 +310,18 @@ mod tests { drop(server); cleanup().unwrap(); assert!(!linked.exists()); + + let (retry, _) = installation("example.retry"); + std::fs::remove_dir_all(retry.join("checkout")).unwrap(); + std::fs::write(retry.join("checkout"), "not a directory").unwrap(); + assert!(cleanup().is_err()); + assert!( + retry.join(LEASE_FILE).exists(), + "failed cleanup remains tracked" + ); + std::fs::remove_file(retry.join("checkout")).unwrap(); + cleanup().unwrap(); + assert!(!retry.exists(), "a later cleanup retries the installation"); }); } @@ -310,8 +336,14 @@ mod tests { .open(handoff.join(LEASE_FILE)) .unwrap(); outgoing.lock_shared().unwrap(); + let mut unrelated = Leases::new(); + retain_startup(&mut unrelated, &[], false).unwrap(); + assert!( + unrelated.is_empty(), + "ordinary startup must not inherit other servers' pins" + ); let mut incoming = Leases::new(); - retain_startup(&mut incoming, &[restored.join("checkout")]).unwrap(); + retain_startup(&mut incoming, &[restored.join("checkout")], true).unwrap(); drop(outgoing); cleanup().unwrap(); assert!(restored.exists());