ssh remoting: Use matching versions of remote server binary (#19740)

Thorsten Ball created

This changes the download logic to not fetch the latest version, but to
fetch the version matching the current version of Zed.


Release Notes:

- Changed the update logic of the SSH remote server to not fetch the
latest version for a current channel, but to fetch the version matching
the current Zed version. If Zed is updated, the server is updated too.
If the server is newer than the Zed version an error will be displayed.

Change summary

crates/auto_update/src/auto_update.rs         | 102 ++++++++++++--------
crates/recent_projects/src/ssh_connections.rs |  25 ++++-
crates/remote/src/ssh_session.rs              |  29 ++++-
3 files changed, 100 insertions(+), 56 deletions(-)

Detailed changes

crates/auto_update/src/auto_update.rs 🔗

@@ -432,10 +432,11 @@ impl AutoUpdater {
         cx.notify();
     }
 
-    pub async fn get_latest_remote_server_release(
+    pub async fn download_remote_server_release(
         os: &str,
         arch: &str,
-        mut release_channel: ReleaseChannel,
+        release_channel: ReleaseChannel,
+        version: Option<SemanticVersion>,
         cx: &mut AsyncAppContext,
     ) -> Result<PathBuf> {
         let this = cx.update(|cx| {
@@ -445,15 +446,12 @@ impl AutoUpdater {
                 .ok_or_else(|| anyhow!("auto-update not initialized"))
         })??;
 
-        if release_channel == ReleaseChannel::Dev {
-            release_channel = ReleaseChannel::Nightly;
-        }
-
-        let release = Self::get_latest_release(
+        let release = Self::get_release(
             &this,
             "zed-remote-server",
             os,
             arch,
+            version,
             Some(release_channel),
             cx,
         )
@@ -468,17 +466,21 @@ impl AutoUpdater {
         let client = this.read_with(cx, |this, _| this.http_client.clone())?;
 
         if smol::fs::metadata(&version_path).await.is_err() {
-            log::info!("downloading zed-remote-server {os} {arch}");
+            log::info!(
+                "downloading zed-remote-server {os} {arch} version {}",
+                release.version
+            );
             download_remote_server_binary(&version_path, release, client, cx).await?;
         }
 
         Ok(version_path)
     }
 
-    pub async fn get_latest_remote_server_release_url(
+    pub async fn get_remote_server_release_url(
         os: &str,
         arch: &str,
-        mut release_channel: ReleaseChannel,
+        release_channel: ReleaseChannel,
+        version: Option<SemanticVersion>,
         cx: &mut AsyncAppContext,
     ) -> Result<(String, String)> {
         let this = cx.update(|cx| {
@@ -488,15 +490,12 @@ impl AutoUpdater {
                 .ok_or_else(|| anyhow!("auto-update not initialized"))
         })??;
 
-        if release_channel == ReleaseChannel::Dev {
-            release_channel = ReleaseChannel::Nightly;
-        }
-
-        let release = Self::get_latest_release(
+        let release = Self::get_release(
             &this,
             "zed-remote-server",
             os,
             arch,
+            version,
             Some(release_channel),
             cx,
         )
@@ -508,46 +507,65 @@ impl AutoUpdater {
         Ok((release.url, body))
     }
 
-    async fn get_latest_release(
+    async fn get_release(
         this: &Model<Self>,
         asset: &str,
         os: &str,
         arch: &str,
+        version: Option<SemanticVersion>,
         release_channel: Option<ReleaseChannel>,
         cx: &mut AsyncAppContext,
     ) -> Result<JsonRelease> {
         let client = this.read_with(cx, |this, _| this.http_client.clone())?;
-        let mut url_string = client.build_url(&format!(
-            "/api/releases/latest?asset={}&os={}&arch={}",
-            asset, os, arch
-        ));
-        if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
-            url_string += "&";
-            url_string += param;
-        }
 
-        let mut response = client.get(&url_string, Default::default(), true).await?;
+        if let Some(version) = version {
+            let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");
 
-        let mut body = Vec::new();
-        response
-            .body_mut()
-            .read_to_end(&mut body)
-            .await
-            .context("error reading release")?;
+            let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);
+
+            Ok(JsonRelease {
+                version: version.to_string(),
+                url: client.build_url(&url),
+            })
+        } else {
+            let mut url_string = client.build_url(&format!(
+                "/api/releases/latest?asset={}&os={}&arch={}",
+                asset, os, arch
+            ));
+            if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
+                url_string += "&";
+                url_string += param;
+            }
+
+            let mut response = client.get(&url_string, Default::default(), true).await?;
+            let mut body = Vec::new();
+            response.body_mut().read_to_end(&mut body).await?;
+
+            if !response.status().is_success() {
+                return Err(anyhow!(
+                    "failed to fetch release: {:?}",
+                    String::from_utf8_lossy(&body),
+                ));
+            }
 
-        if !response.status().is_success() {
-            Err(anyhow!(
-                "failed to fetch release: {:?}",
-                String::from_utf8_lossy(&body),
-            ))?;
+            serde_json::from_slice(body.as_slice()).with_context(|| {
+                format!(
+                    "error deserializing release {:?}",
+                    String::from_utf8_lossy(&body),
+                )
+            })
         }
+    }
 
-        serde_json::from_slice(body.as_slice()).with_context(|| {
-            format!(
-                "error deserializing release {:?}",
-                String::from_utf8_lossy(&body),
-            )
-        })
+    async fn get_latest_release(
+        this: &Model<Self>,
+        asset: &str,
+        os: &str,
+        arch: &str,
+        release_channel: Option<ReleaseChannel>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<JsonRelease> {
+        Self::get_release(this, asset, os, arch, None, release_channel, cx).await
     }
 
     async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -517,17 +517,31 @@ impl SshClientDelegate {
             }
         }
 
+        // For nightly channel, always get latest
+        let current_version = if release_channel == ReleaseChannel::Nightly {
+            None
+        } else {
+            Some(version)
+        };
+
+        self.update_status(
+            Some(&format!("Checking remote server release {}", version)),
+            cx,
+        );
+
         if download_binary_on_host {
-            let (request_url, request_body) = AutoUpdater::get_latest_remote_server_release_url(
+            let (request_url, request_body) = AutoUpdater::get_remote_server_release_url(
                 platform.os,
                 platform.arch,
                 release_channel,
+                current_version,
                 cx,
             )
             .await
             .map_err(|e| {
                 anyhow!(
-                    "Failed to get remote server binary download url (os: {}, arch: {}): {}",
+                    "Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
+                    version,
                     platform.os,
                     platform.arch,
                     e
@@ -542,17 +556,18 @@ impl SshClientDelegate {
                 version,
             ))
         } else {
-            self.update_status(Some("Checking for latest version of remote server"), cx);
-            let binary_path = AutoUpdater::get_latest_remote_server_release(
+            let binary_path = AutoUpdater::download_remote_server_release(
                 platform.os,
                 platform.arch,
                 release_channel,
+                current_version,
                 cx,
             )
             .await
             .map_err(|e| {
                 anyhow!(
-                    "Failed to download remote server binary (os: {}, arch: {}): {}",
+                    "Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
+                    version,
                     platform.os,
                     platform.arch,
                     e

crates/remote/src/ssh_session.rs 🔗

@@ -1707,21 +1707,32 @@ impl SshRemoteConnection {
 
         let (binary, version) = delegate.get_server_binary(platform, cx).await??;
 
-        let mut server_binary_exists = false;
-        if !server_binary_exists && cfg!(not(debug_assertions)) {
+        let mut remote_version = None;
+        if cfg!(not(debug_assertions)) {
             if let Ok(installed_version) =
                 run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
             {
-                if installed_version.trim() == version.to_string() {
-                    server_binary_exists = true;
+                if let Ok(version) = installed_version.trim().parse::<SemanticVersion>() {
+                    remote_version = Some(version);
+                } else {
+                    log::warn!("failed to parse version of remote server: {installed_version:?}",);
                 }
-                log::info!("checked remote server binary for version. latest version: {}. remote server version: {}", version.to_string(), installed_version.trim());
             }
-        }
 
-        if server_binary_exists {
-            log::info!("remote development server already present",);
-            return Ok(());
+            if let Some(remote_version) = remote_version {
+                if remote_version == version {
+                    log::info!("remote development server present and matching client version");
+                    return Ok(());
+                } else if remote_version > version {
+                    let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", remote_version, version);
+                    return Err(error);
+                } else {
+                    log::info!(
+                        "remote development server has older version: {}. updating...",
+                        remote_version
+                    );
+                }
+            }
         }
 
         match binary {