From af8641ce5bbbe4c0c3c68d30e0c8a488b854a2d7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 22 May 2024 21:25:38 -0600 Subject: [PATCH] reconnect ssh (#12147) Release Notes: - N/A --------- Co-authored-by: Bennet --- Cargo.lock | 1 + crates/recent_projects/Cargo.toml | 1 + crates/recent_projects/src/dev_servers.rs | 207 ++++++++++++------ crates/recent_projects/src/recent_projects.rs | 146 +++++++----- 4 files changed, 230 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a03d080b70..aaa8cbb669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8067,6 +8067,7 @@ name = "recent_projects" version = "0.1.0" dependencies = [ "anyhow", + "client", "dev_server_projects", "editor", "feature_flags", diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index a78c90d6b5..47277f2a06 100644 --- a/crates/recent_projects/Cargo.toml +++ b/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 diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 212020253b..21d424bbd5 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/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; + 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::(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, + 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, + dev_server: DevServer, + cx: &mut WindowContext, +) -> Task> { + 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, + dev_server_store: Model, + 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::(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(()) +} diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index edec370216..d4ba73a696 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/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::() - } else { - None - }; + 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?; - 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?; + 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, +) -> Task> { + if let Some(app_state) = AppState::global(cx).upgrade() { + let handle = if replace_current_window { + cx.window_handle().downcast::() + } 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,