reconnect ssh (#12147)

Conrad Irwin and Bennet created

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>

Change summary

Cargo.lock                                    |   1 
crates/recent_projects/Cargo.toml             |   1 
crates/recent_projects/src/dev_servers.rs     | 209 +++++++++++++-------
crates/recent_projects/src/recent_projects.rs | 148 +++++++++-----
4 files changed, 232 insertions(+), 127 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8067,6 +8067,7 @@ name = "recent_projects"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "client",
  "dev_server_projects",
  "editor",
  "feature_flags",

crates/recent_projects/Cargo.toml 🔗

@@ -14,6 +14,7 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
+client.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 fuzzy.workspace = true

crates/recent_projects/src/dev_servers.rs 🔗

@@ -1,10 +1,12 @@
 use std::time::Duration;
 
+use anyhow::anyhow;
 use anyhow::Context;
 use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
 use editor::Editor;
 use feature_flags::FeatureFlagAppExt;
 use feature_flags::FeatureFlagViewExt;
+use gpui::AsyncWindowContext;
 use gpui::Subscription;
 use gpui::Task;
 use gpui::WeakView;
@@ -312,91 +314,58 @@ impl DevServerProjects {
         });
 
         let workspace = self.workspace.clone();
+        let store = dev_server_projects::Store::global(cx);
 
         cx.spawn({
-            let access_token = access_token.clone();
             |this, mut cx| async move {
-            let result = dev_server.await;
-
-            match result {
-                Ok(dev_server) => {
-                    if let Some(ssh_connection_string) =  ssh_connection_string {
-
-                        let access_token = access_token.clone();
-                        this.update(&mut cx, |this, cx| {
-                                this.focus_handle.focus(cx);
-                                this.mode = Mode::CreateDevServer(CreateDevServer {
-                                    creating: true,
-                                    dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
-                                    access_token: Some(access_token.unwrap_or(dev_server.access_token.clone())),
-                                    manual_setup: false,
-                            });
-                                cx.notify();
-                        })?;
-                    let terminal_panel = workspace
-                        .update(&mut cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
-                        .ok()
-                        .flatten()
-                        .with_context(|| anyhow::anyhow!("No terminal panel"))?;
-
-                        let command = "sh".to_string();
-                        let args = vec!["-x".to_string(),"-c".to_string(),
-                            format!(r#"~/.local/bin/zed -v >/dev/stderr || (curl -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#, dev_server.access_token)];
-
-                        let terminal = terminal_panel.update(&mut cx, |terminal_panel, cx| {
-                            terminal_panel.spawn_in_new_terminal(
-                                SpawnInTerminal {
-                                    id: task::TaskId("ssh-remote".into()),
-                                    full_label: "Install zed over ssh".into(),
-                                    label: "Install zed over ssh".into(),
-                                    command,
-                                    args,
-                                    command_label: ssh_connection_string.clone(),
-                                    cwd: Some(TerminalWorkDir::Ssh { ssh_command: ssh_connection_string, path: None }),
-                                    env: Default::default(),
-                                    use_new_terminal: true,
-                                    allow_concurrent_runs: false,
-                                    reveal: RevealStrategy::Always,
-                                },
-                                cx,
+                let result = dev_server.await;
+
+                match result {
+                    Ok(dev_server) => {
+                        if let Some(ssh_connection_string) = ssh_connection_string {
+                            spawn_ssh_task(
+                                workspace
+                                    .upgrade()
+                                    .ok_or_else(|| anyhow!("workspace dropped"))?,
+                                store,
+                                DevServerId(dev_server.dev_server_id),
+                                ssh_connection_string,
+                                dev_server.access_token.clone(),
+                                &mut cx,
                             )
-                        })?.await?;
-
-                        terminal.update(&mut cx, |terminal, cx| {
-                            terminal.wait_for_completed_task(cx)
-                        })?.await;
-
-                        // There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
-                        if this.update(&mut cx, |this, cx| {
-                            this.dev_server_store.read(cx).dev_server_status(DevServerId(dev_server.dev_server_id))
-                        })? == DevServerStatus::Offline {
-                            cx.background_executor().timer(Duration::from_millis(200)).await
+                            .await
+                            .log_err();
                         }
-                    }
 
-                    this.update(&mut cx, |this, cx| {
+                        this.update(&mut cx, |this, cx| {
                             this.focus_handle.focus(cx);
                             this.mode = Mode::CreateDevServer(CreateDevServer {
                                 creating: false,
                                 dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
                                 access_token: Some(dev_server.access_token),
                                 manual_setup,
-                        });
+                            });
                             cx.notify();
-                    })?;
-                Ok(())
-            }
-            Err(e) => {
-                this.update(&mut cx, |this, cx| {
-                    this.mode = Mode::CreateDevServer(CreateDevServer { creating:false, dev_server_id: existing_id, access_token: None, manual_setup });
-                    cx.notify()
-                })
-                .log_err();
+                        })?;
+                        Ok(())
+                    }
+                    Err(e) => {
+                        this.update(&mut cx, |this, cx| {
+                            this.mode = Mode::CreateDevServer(CreateDevServer {
+                                creating: false,
+                                dev_server_id: existing_id,
+                                access_token: None,
+                                manual_setup,
+                            });
+                            cx.notify()
+                        })
+                        .log_err();
 
-                return Err(e)
-            }
+                        return Err(e);
+                    }
+                }
             }
-        }})
+        })
         .detach_and_prompt_err("Failed to create server", cx, |_, _| None);
 
         self.mode = Mode::CreateDevServer(CreateDevServer {
@@ -1021,3 +990,103 @@ impl Render for DevServerProjects {
             })
     }
 }
+
+pub fn reconnect_to_dev_server(
+    workspace: View<Workspace>,
+    dev_server: DevServer,
+    cx: &mut WindowContext,
+) -> Task<anyhow::Result<()>> {
+    let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
+        return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
+    };
+    let dev_server_store = dev_server_projects::Store::global(cx);
+    let get_access_token = dev_server_store.update(cx, |store, cx| {
+        store.regenerate_dev_server_token(dev_server.id, cx)
+    });
+
+    cx.spawn(|mut cx| async move {
+        let access_token = get_access_token.await?.access_token;
+
+        spawn_ssh_task(
+            workspace,
+            dev_server_store,
+            dev_server.id,
+            ssh_connection_string.to_string(),
+            access_token,
+            &mut cx,
+        )
+        .await
+    })
+}
+
+pub async fn spawn_ssh_task(
+    workspace: View<Workspace>,
+    dev_server_store: Model<dev_server_projects::Store>,
+    dev_server_id: DevServerId,
+    ssh_connection_string: String,
+    access_token: String,
+    cx: &mut AsyncWindowContext,
+) -> anyhow::Result<()> {
+    let terminal_panel = workspace
+        .update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
+        .ok()
+        .flatten()
+        .with_context(|| anyhow!("No terminal panel"))?;
+
+    let command = "sh".to_string();
+    let args = vec![
+        "-x".to_string(),
+        "-c".to_string(),
+        format!(
+            r#"~/.local/bin/zed -v >/dev/stderr || (curl -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#,
+            access_token
+        ),
+    ];
+
+    let ssh_connection_string = ssh_connection_string.to_string();
+
+    let terminal = terminal_panel
+        .update(cx, |terminal_panel, cx| {
+            terminal_panel.spawn_in_new_terminal(
+                SpawnInTerminal {
+                    id: task::TaskId("ssh-remote".into()),
+                    full_label: "Install zed over ssh".into(),
+                    label: "Install zed over ssh".into(),
+                    command,
+                    args,
+                    command_label: ssh_connection_string.clone(),
+                    cwd: Some(TerminalWorkDir::Ssh {
+                        ssh_command: ssh_connection_string,
+                        path: None,
+                    }),
+                    env: Default::default(),
+                    use_new_terminal: true,
+                    allow_concurrent_runs: false,
+                    reveal: RevealStrategy::Always,
+                },
+                cx,
+            )
+        })?
+        .await?;
+
+    terminal
+        .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
+        .await;
+
+    // There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
+    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
+        == DevServerStatus::Offline
+    {
+        cx.background_executor()
+            .timer(Duration::from_millis(200))
+            .await
+    }
+
+    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
+        == DevServerStatus::Offline
+    {
+        return Err(anyhow!("couldn't reconnect"))?;
+    }
+
+    Ok(())
+}

crates/recent_projects/src/recent_projects.rs 🔗

@@ -1,5 +1,7 @@
 mod dev_servers;
 
+use client::ProjectId;
+use dev_servers::reconnect_to_dev_server;
 pub use dev_servers::DevServerProjects;
 use feature_flags::FeatureFlagAppExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
@@ -17,6 +19,7 @@ use serde::Deserialize;
 use std::{
     path::{Path, PathBuf},
     sync::Arc,
+    time::Duration,
 };
 use ui::{
     prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
@@ -313,73 +316,59 @@ impl PickerDelegate for RecentProjectsDelegate {
                                 }
                             }
                             SerializedWorkspaceLocation::DevServer(dev_server_project) => {
-                                let store = dev_server_projects::Store::global(cx).read(cx);
-                                let Some(project_id) = store
+                                let store = dev_server_projects::Store::global(cx);
+                                let Some(project_id) = store.read(cx)
                                     .dev_server_project(dev_server_project.id)
                                     .and_then(|p| p.project_id)
                                 else {
-                                    let dev_server_name = dev_server_project.dev_server_name.clone();
-                                    return cx.spawn(|workspace, mut cx| async move {
-                                        let response =
-                                            cx.prompt(gpui::PromptLevel::Warning,
-                                                "Dev Server is offline",
-                                                Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
-                                                &["Ok", "Open Settings"]
-                                            ).await?;
-                                        if response == 1 {
-                                            workspace.update(&mut cx, |workspace, cx| {
-                                                let handle = cx.view().downgrade();
-                                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
-                                            })?;
-                                        } else {
-                                            workspace.update(&mut cx, |workspace, cx| {
-                                                RecentProjects::open(workspace, true, cx);
-                                            })?;
-                                        }
-                                        Ok(())
-                                    })
-                                };
-                                if let Some(app_state) = AppState::global(cx).upgrade() {
-                                    let handle = if replace_current_window {
-                                        cx.window_handle().downcast::<Workspace>()
-                                    } else {
-                                        None
-                                    };
-
-                                    if let Some(handle) = handle {
-                                            cx.spawn(move |workspace, mut cx| async move {
-                                                let continue_replacing = workspace
-                                                    .update(&mut cx, |workspace, cx| {
-                                                        workspace.
-                                                            prepare_to_close(true, cx)
-                                                    })?
-                                                    .await?;
-                                                if continue_replacing {
-                                                    workspace
-                                                        .update(&mut cx, |_workspace, cx| {
-                                                            workspace::join_dev_server_project(project_id, app_state, Some(handle), cx)
-                                                        })?
-                                                        .await?;
+                                    let server = store.read(cx).dev_server_for_project(dev_server_project.id);
+                                    if server.is_some_and(|server| server.ssh_connection_string.is_some()) {
+                                        let reconnect =  reconnect_to_dev_server(cx.view().clone(), server.unwrap().clone(), cx);
+                                        let id = dev_server_project.id;
+                                        return cx.spawn(|workspace, mut cx| async move {
+                                            reconnect.await?;
+
+                                            cx.background_executor().timer(Duration::from_millis(1000)).await;
+
+                                            if let Some(project_id) = store.update(&mut cx, |store, _| {
+                                                store.dev_server_project(id)
+                                                    .and_then(|p| p.project_id)
+                                            })? {
+                                                    workspace.update(&mut cx, move |_, cx| {
+                                                    open_dev_server_project(replace_current_window, project_id, cx)
+                                                    })?.await?;
                                                 }
-                                                Ok(())
-                                            })
-                                        }
-                                    else {
-                                        let task =
-                                            workspace::join_dev_server_project(project_id, app_state, None, cx);
-                                        cx.spawn(|_, _| async move {
-                                            task.await?;
+                                            Ok(())
+                                        })
+                                    } else {
+                                        let dev_server_name = dev_server_project.dev_server_name.clone();
+                                        return cx.spawn(|workspace, mut cx| async move {
+                                            let response =
+                                                cx.prompt(gpui::PromptLevel::Warning,
+                                                    "Dev Server is offline",
+                                                    Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
+                                                    &["Ok", "Open Settings"]
+                                                ).await?;
+                                            if response == 1 {
+                                                workspace.update(&mut cx, |workspace, cx| {
+                                                    let handle = cx.view().downgrade();
+                                                    workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
+                                                })?;
+                                            } else {
+                                                workspace.update(&mut cx, |workspace, cx| {
+                                                    RecentProjects::open(workspace, true, cx);
+                                                })?;
+                                            }
                                             Ok(())
                                         })
                                     }
-                                } else {
-                                    Task::ready(Err(anyhow::anyhow!("App state not found")))
-                                }
-                            }
+                                };
+                                open_dev_server_project(replace_current_window, project_id, cx)
                         }
                     }
+                }
                 })
-                .detach_and_log_err(cx);
+            .detach_and_log_err(cx);
             cx.emit(DismissEvent);
         }
     }
@@ -563,6 +552,51 @@ impl PickerDelegate for RecentProjectsDelegate {
     }
 }
 
+fn open_dev_server_project(
+    replace_current_window: bool,
+    project_id: ProjectId,
+    cx: &mut ViewContext<Workspace>,
+) -> Task<anyhow::Result<()>> {
+    if let Some(app_state) = AppState::global(cx).upgrade() {
+        let handle = if replace_current_window {
+            cx.window_handle().downcast::<Workspace>()
+        } else {
+            None
+        };
+
+        if let Some(handle) = handle {
+            cx.spawn(move |workspace, mut cx| async move {
+                let continue_replacing = workspace
+                    .update(&mut cx, |workspace, cx| {
+                        workspace.prepare_to_close(true, cx)
+                    })?
+                    .await?;
+                if continue_replacing {
+                    workspace
+                        .update(&mut cx, |_workspace, cx| {
+                            workspace::join_dev_server_project(
+                                project_id,
+                                app_state,
+                                Some(handle),
+                                cx,
+                            )
+                        })?
+                        .await?;
+                }
+                Ok(())
+            })
+        } else {
+            let task = workspace::join_dev_server_project(project_id, app_state, None, cx);
+            cx.spawn(|_, _| async move {
+                task.await?;
+                Ok(())
+            })
+        }
+    } else {
+        Task::ready(Err(anyhow::anyhow!("App state not found")))
+    }
+}
+
 // Compute the highlighted text for the name and path
 fn highlights_for_path(
     path: &Path,