From 98da1ea169f3bc35248aa196904b73cad5927f6a Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 17 Nov 2025 14:13:58 -0800 Subject: [PATCH] Fix remote extension syncing (#42918) Closes #40906 Closes #39729 SFTP uploads weren't quoting the install directory which was causing extension syncing to fail. We were also only running `install_extension` once per remote-connection instead of once per project (thx @feeiyu for pointing this out) so extension weren't being loaded in subsequently opened remote projects. Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- crates/extension_host/src/extension_host.rs | 56 ++++++++++++--------- crates/extension_host/src/headless_host.rs | 3 +- crates/remote/src/transport/ssh.rs | 2 +- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 89ea83a905d75ae546125cd7479860bea9f21f9c..2c99acbc7f4028598ab110c54d7f0cb4714d2c67 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::ExtensionProvides; use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry}; -use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; +use collections::{BTreeMap, BTreeSet, HashSet, btree_map}; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ @@ -43,7 +43,7 @@ use language::{ use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; -use remote::{RemoteClient, RemoteConnectionOptions}; +use remote::RemoteClient; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -123,7 +123,7 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, - pub remote_clients: HashMap>, + pub remote_clients: Vec>, pub ssh_registered_tx: UnboundedSender<()>, } @@ -274,7 +274,7 @@ impl ExtensionStore { reload_tx, tasks: Vec::new(), - remote_clients: HashMap::default(), + remote_clients: Default::default(), ssh_registered_tx: connection_registered_tx, }; @@ -348,7 +348,7 @@ impl ExtensionStore { index_changed = false; } - Self::update_ssh_clients(&this, cx).await?; + Self::update_remote_clients(&this, cx).await?; } _ = connection_registered_rx.next() => { debounce_timer = cx @@ -1725,7 +1725,7 @@ impl ExtensionStore { }) } - async fn sync_extensions_over_ssh( + async fn sync_extensions_to_remotes( this: &WeakEntity, client: WeakEntity, cx: &mut AsyncApp, @@ -1778,7 +1778,11 @@ impl ExtensionStore { })?, path_style, ); - log::info!("Uploading extension {}", missing_extension.clone().id); + log::info!( + "Uploading extension {} to {:?}", + missing_extension.clone().id, + dest_dir + ); client .update(cx, |client, cx| { @@ -1791,27 +1795,35 @@ impl ExtensionStore { missing_extension.clone().id ); - client + let result = client .update(cx, |client, _cx| { client.proto_client().request(proto::InstallExtension { tmp_dir: dest_dir.to_proto(), - extension: Some(missing_extension), + extension: Some(missing_extension.clone()), }) })? - .await?; + .await; + + if let Err(e) = result { + log::error!( + "Failed to install extension {}: {}", + missing_extension.id, + e + ); + } } anyhow::Ok(()) } - pub async fn update_ssh_clients(this: &WeakEntity, cx: &mut AsyncApp) -> Result<()> { + pub async fn update_remote_clients(this: &WeakEntity, cx: &mut AsyncApp) -> Result<()> { let clients = this.update(cx, |this, _cx| { - this.remote_clients.retain(|_k, v| v.upgrade().is_some()); - this.remote_clients.values().cloned().collect::>() + this.remote_clients.retain(|v| v.upgrade().is_some()); + this.remote_clients.clone() })?; for client in clients { - Self::sync_extensions_over_ssh(this, client, cx) + Self::sync_extensions_to_remotes(this, client, cx) .await .log_err(); } @@ -1819,16 +1831,12 @@ impl ExtensionStore { anyhow::Ok(()) } - pub fn register_remote_client(&mut self, client: Entity, cx: &mut Context) { - let options = client.read(cx).connection_options(); - - if let Some(existing_client) = self.remote_clients.get(&options) - && existing_client.upgrade().is_some() - { - return; - } - - self.remote_clients.insert(options, client.downgrade()); + pub fn register_remote_client( + &mut self, + client: Entity, + _cx: &mut Context, + ) { + self.remote_clients.push(client.downgrade()); self.ssh_registered_tx.unbounded_send(()).ok(); } } diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index f14bb811a6742a60899ac4301cfac096bb41a07f..1fe1905e0b89a89fd8b7304b9a175a832f3ffc29 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -279,7 +279,8 @@ impl HeadlessExtensionStore { } fs.rename(&tmp_path, &path, RenameOptions::default()) - .await?; + .await + .context("Failed to rename {tmp_path:?} to {path:?}")?; Self::load_extension(this, extension, cx).await }) diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index cf8e6f3e9cc9599aa7d2d05ea204c550892ac4c4..8fed12a4cb9d594160732cc94637f8b53692f9cf 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -304,7 +304,7 @@ impl RemoteConnection for SshRemoteConnection { let mut child = sftp_command.spawn()?; if let Some(mut stdin) = child.stdin.take() { use futures::AsyncWriteExt; - let sftp_batch = format!("put -r {src_path_display} {dest_path_str}\n"); + let sftp_batch = format!("put -r \"{src_path_display}\" \"{dest_path_str}\"\n"); stdin.write_all(sftp_batch.as_bytes()).await?; stdin.flush().await?; }