Fixes for SSH remoting infrastructure (#14844)

Max Brunsfeld created

* Fixed mis-named macOS remote server archives in actions and packaging
scripts
* Fixed an issue with the ask pass script on linux
* Download nightly versions of remote servers in dev mode (not stable)

Release Notes:

- N/A

Change summary

.github/workflows/ci.yml              |  4 +-
crates/auto_update/src/auto_update.rs | 48 +++++++++++++++++++++-------
crates/remote/src/ssh_session.rs      | 30 ++++++++++-------
crates/zed/src/zed/open_listener.rs   |  2 
script/bundle-mac                     |  4 +-
5 files changed, 57 insertions(+), 31 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -251,8 +251,8 @@ jobs:
           draft: true
           prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
           files: |
-            target/zed-remote-server-mac-x86_64.gz
-            target/zed-remote-server-mac-aarch64.gz
+            target/zed-remote-server-macos-x86_64.gz
+            target/zed-remote-server-macos-aarch64.gz
             target/aarch64-apple-darwin/release/Zed-aarch64.dmg
             target/x86_64-apple-darwin/release/Zed-x86_64.dmg
             target/release/Zed.dmg

crates/auto_update/src/auto_update.rs 🔗

@@ -401,7 +401,15 @@ impl AutoUpdater {
             release_channel = ReleaseChannel::Nightly;
         }
 
-        let release = Self::get_latest_release(&this, "zed-remote-server", os, arch, cx).await?;
+        let release = Self::get_latest_release(
+            &this,
+            "zed-remote-server",
+            os,
+            arch,
+            Some(release_channel),
+            cx,
+        )
+        .await?;
 
         let servers_dir = paths::remote_servers_dir();
         let channel_dir = servers_dir.join(release_channel.dev_name());
@@ -423,6 +431,7 @@ impl AutoUpdater {
         asset: &str,
         os: &str,
         arch: &str,
+        release_channel: Option<ReleaseChannel>,
         cx: &mut AsyncAppContext,
     ) -> Result<JsonRelease> {
         let client = this.read_with(cx, |this, _| this.http_client.clone())?;
@@ -430,14 +439,10 @@ impl AutoUpdater {
             "/api/releases/latest?asset={}&os={}&arch={}",
             asset, os, arch
         ));
-        cx.update(|cx| {
-            if let Some(param) = ReleaseChannel::try_global(cx)
-                .and_then(|release_channel| release_channel.release_query_param())
-            {
-                url_string += "&";
-                url_string += param;
-            }
-        })?;
+        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?;
 
@@ -448,17 +453,34 @@ impl AutoUpdater {
             .await
             .context("error reading release")?;
 
-        serde_json::from_slice(body.as_slice()).context("error deserializing release")
+        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),
+            )
+        })
     }
 
     async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
-        let (client, current_version) = this.update(&mut cx, |this, cx| {
+        let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
             this.status = AutoUpdateStatus::Checking;
             cx.notify();
-            (this.http_client.clone(), this.current_version)
+            (
+                this.http_client.clone(),
+                this.current_version,
+                ReleaseChannel::try_global(cx),
+            )
         })?;
 
-        let release = Self::get_latest_release(&this, "zed", OS, ARCH, &mut cx).await?;
+        let release =
+            Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
 
         let should_download = match *RELEASE_CHANNEL {
             ReleaseChannel::Nightly => cx

crates/remote/src/ssh_session.rs 🔗

@@ -426,7 +426,8 @@ impl SshClientState {
         delegate: Arc<dyn SshClientDelegate>,
         cx: &AsyncAppContext,
     ) -> Result<Self> {
-        use smol::fs::unix::PermissionsExt as _;
+        use futures::{io::BufReader, AsyncBufReadExt as _};
+        use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
         use util::ResultExt as _;
 
         let url = format!("{user}@{host}");
@@ -434,15 +435,16 @@ impl SshClientState {
             .prefix("zed-ssh-session")
             .tempdir()?;
 
-        // Create a TCP listener to handle requests from the askpass program.
-        let listener = smol::net::TcpListener::bind("127.0.0.1:0")
-            .await
-            .expect("failed to find open port");
-        let askpass_port = listener.local_addr().unwrap().port();
+        // Create a domain socket listener to handle requests from the askpass program.
+        let askpass_socket = temp_dir.path().join("askpass.sock");
+        let listener =
+            UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
+
         let askpass_task = cx.spawn(|mut cx| async move {
             while let Ok((mut stream, _)) = listener.accept().await {
                 let mut buffer = Vec::new();
-                if stream.read_to_end(&mut buffer).await.is_err() {
+                let mut reader = BufReader::new(&mut stream);
+                if reader.read_until(b'\0', &mut buffer).await.is_err() {
                     buffer.clear();
                 }
                 let password_prompt = String::from_utf8_lossy(&buffer);
@@ -458,10 +460,12 @@ impl SshClientState {
             }
         });
 
-        // Create an askpass script that communicates back to this process using TCP.
+        // Create an askpass script that communicates back to this process.
         let askpass_script = format!(
-            "{shebang}\n echo \"$@\" | nc 127.0.0.1 {askpass_port} 2> /dev/null",
-            shebang = "#!/bin/sh"
+            "{shebang}\n{print_args} | nc -U {askpass_socket} 2> /dev/null \n",
+            askpass_socket = askpass_socket.display(),
+            print_args = "printf '%s\\0' \"$@\"",
+            shebang = "#!/bin/sh",
         );
         let askpass_script_path = temp_dir.path().join("askpass.sh");
         fs::write(&askpass_script_path, askpass_script).await?;
@@ -501,11 +505,11 @@ impl SshClientState {
         }
 
         Ok(Self {
-            _master_process: master_process,
+            url,
             port,
-            _temp_dir: temp_dir,
             socket_path,
-            url,
+            _master_process: master_process,
+            _temp_dir: temp_dir,
         })
     }
 

crates/zed/src/zed/open_listener.rs 🔗

@@ -177,7 +177,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
                     workspace.toggle_modal(cx, |cx| PasswordPrompt::new(prompt, tx, cx));
                 }
             })
-            .unwrap();
+            .ok();
         rx
     }
 

script/bundle-mac 🔗

@@ -390,6 +390,6 @@ else
 
     sign_binary "target/x86_64-apple-darwin/release/remote_server"
     sign_binary "target/aarch64-apple-darwin/release/remote_server"
-    gzip --stdout --best target/x86_64-apple-darwin/release/remote_server > target/zed-remote-server-mac-x86_64.gz
-    gzip --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-mac-aarch64.gz
+    gzip --stdout --best target/x86_64-apple-darwin/release/remote_server > target/zed-remote-server-macos-x86_64.gz
+    gzip --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-macos-aarch64.gz
 fi