Remoting: Fix connecting to servers with long hostnames (#20093)

Conrad Irwin created

Closes #20018

Release Notes:

- Remoting: Fixed connecting to hosts with long (>~50 character)
hostnames

Change summary

Cargo.lock                                       |  1 
crates/recent_projects/src/remote_servers.rs     |  5 +
crates/recent_projects/src/ssh_connections.rs    |  3 
crates/remote/src/ssh_session.rs                 | 52 +++++++++++++----
crates/remote_server/src/remote_editing_tests.rs |  5 +
crates/workspace/Cargo.toml                      |  1 
crates/workspace/src/workspace.rs                | 17 -----
7 files changed, 50 insertions(+), 34 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14765,7 +14765,6 @@ dependencies = [
  "parking_lot",
  "postage",
  "project",
- "release_channel",
  "remote",
  "schemars",
  "serde",

crates/recent_projects/src/remote_servers.rs 🔗

@@ -16,6 +16,7 @@ use gpui::{
 };
 use picker::Picker;
 use project::Project;
+use remote::ssh_session::ConnectionIdentifier;
 use remote::SshConnectionOptions;
 use remote::SshRemoteClient;
 use settings::update_settings_file;
@@ -384,7 +385,7 @@ impl RemoteServerProjects {
         let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
 
         let connection = connect_over_ssh(
-            connection_options.remote_server_identifier(),
+            ConnectionIdentifier::Setup,
             connection_options.clone(),
             ssh_prompt.clone(),
             cx,
@@ -475,7 +476,7 @@ impl RemoteServerProjects {
                     .clone();
 
                 let connect = connect_over_ssh(
-                    connection_options.remote_server_identifier(),
+                    ConnectionIdentifier::Setup,
                     connection_options.clone(),
                     prompt,
                     cx,

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -14,6 +14,7 @@ use gpui::{AppContext, Model};
 use language::CursorShape;
 use markdown::{Markdown, MarkdownStyle};
 use release_channel::ReleaseChannel;
+use remote::ssh_session::ConnectionIdentifier;
 use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -534,7 +535,7 @@ pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {
 }
 
 pub fn connect_over_ssh(
-    unique_identifier: String,
+    unique_identifier: ConnectionIdentifier,
     connection_options: SshConnectionOptions,
     ui: View<SshPrompt>,
     cx: &mut WindowContext,

crates/remote/src/ssh_session.rs 🔗

@@ -202,17 +202,6 @@ impl SshConnectionOptions {
             host
         }
     }
-
-    // Uniquely identifies dev server projects on a remote host. Needs to be
-    // stable for the same dev server project.
-    pub fn remote_server_identifier(&self) -> String {
-        let mut identifier = format!("dev-server-{:?}", self.host);
-        if let Some(username) = self.username.as_ref() {
-            identifier.push('-');
-            identifier.push_str(&username);
-        }
-        identifier
-    }
 }
 
 #[derive(Copy, Clone, Debug)]
@@ -520,14 +509,43 @@ pub enum SshRemoteEvent {
 
 impl EventEmitter<SshRemoteEvent> for SshRemoteClient {}
 
+// Identifies the socket on the remote server so that reconnects
+// can re-join the same project.
+pub enum ConnectionIdentifier {
+    Setup,
+    Workspace(i64),
+}
+
+impl ConnectionIdentifier {
+    // This string gets used in a socket name, and so must be relatively short.
+    // The total length of:
+    //   /home/{username}/.local/share/zed/server_state/{name}/stdout.sock
+    // Must be less than about 100 characters
+    //   https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars
+    // So our strings should be at most 20 characters or so.
+    fn to_string(&self, cx: &AppContext) -> String {
+        let identifier_prefix = match ReleaseChannel::global(cx) {
+            ReleaseChannel::Stable => "".to_string(),
+            release_channel => format!("{}-", release_channel.dev_name()),
+        };
+        match self {
+            Self::Setup => format!("{identifier_prefix}setup"),
+            Self::Workspace(workspace_id) => {
+                format!("{identifier_prefix}workspace-{workspace_id}",)
+            }
+        }
+    }
+}
+
 impl SshRemoteClient {
     pub fn new(
-        unique_identifier: String,
+        unique_identifier: ConnectionIdentifier,
         connection_options: SshConnectionOptions,
         cancellation: oneshot::Receiver<()>,
         delegate: Arc<dyn SshClientDelegate>,
         cx: &mut AppContext,
     ) -> Task<Result<Option<Model<Self>>>> {
+        let unique_identifier = unique_identifier.to_string(cx);
         cx.spawn(|mut cx| async move {
             let success = Box::pin(async move {
                 let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
@@ -1096,7 +1114,15 @@ impl SshRemoteClient {
     ) -> Model<Self> {
         let (_tx, rx) = oneshot::channel();
         client_cx
-            .update(|cx| Self::new("fake".to_string(), opts, rx, Arc::new(fake::Delegate), cx))
+            .update(|cx| {
+                Self::new(
+                    ConnectionIdentifier::Setup,
+                    opts,
+                    rx,
+                    Arc::new(fake::Delegate),
+                    cx,
+                )
+            })
             .await
             .unwrap()
             .unwrap()

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -2,7 +2,7 @@ use crate::headless_project::HeadlessProject;
 use client::{Client, UserStore};
 use clock::FakeSystemClock;
 use fs::{FakeFs, Fs};
-use gpui::{Context, Model, TestAppContext};
+use gpui::{Context, Model, SemanticVersion, TestAppContext};
 use http_client::{BlockedHttpClient, FakeHttpClient};
 use language::{
     language_settings::{language_settings, AllLanguageSettings},
@@ -1184,6 +1184,9 @@ pub async fn init_test(
     server_cx: &mut TestAppContext,
 ) -> (Model<Project>, Model<HeadlessProject>) {
     let server_fs = server_fs.clone();
+    cx.update(|cx| {
+        release_channel::init(SemanticVersion::default(), cx);
+    });
     init_logger();
 
     let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);

crates/workspace/Cargo.toml 🔗

@@ -49,7 +49,6 @@ parking_lot.workspace = true
 postage.workspace = true
 project.workspace = true
 task.workspace = true
-release_channel.workspace = true
 remote.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/workspace/src/workspace.rs 🔗

@@ -64,8 +64,7 @@ use postage::stream::Stream;
 use project::{
     DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
 };
-use release_channel::ReleaseChannel;
-use remote::{SshClientDelegate, SshConnectionOptions};
+use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
 use serde::Deserialize;
 use session::AppSession;
 use settings::Settings;
@@ -5496,26 +5495,14 @@ pub fn open_ssh_project(
     paths: Vec<PathBuf>,
     cx: &mut AppContext,
 ) -> Task<Result<()>> {
-    let release_channel = ReleaseChannel::global(cx);
-
     cx.spawn(|mut cx| async move {
         let (serialized_ssh_project, workspace_id, serialized_workspace) =
             serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
 
-        let identifier_prefix = match release_channel {
-            ReleaseChannel::Stable => None,
-            _ => Some(format!("{}-", release_channel.dev_name())),
-        };
-        let unique_identifier = format!(
-            "{}workspace-{}",
-            identifier_prefix.unwrap_or_default(),
-            workspace_id.0
-        );
-
         let session = match cx
             .update(|cx| {
                 remote::SshRemoteClient::new(
-                    unique_identifier,
+                    ConnectionIdentifier::Workspace(workspace_id.0),
                     connection_options,
                     cancel_rx,
                     delegate,