reconnect ssh (#12147)

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Conrad Irwin 2024-05-22 21:25:38 -06:00 committed by GitHub
parent ea166f0b27
commit af8641ce5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 230 additions and 125 deletions

1
Cargo.lock generated
View File

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

View File

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

View File

@ -1,10 +1,12 @@
use std::time::Duration; use std::time::Duration;
use anyhow::anyhow;
use anyhow::Context; use anyhow::Context;
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId}; use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
use editor::Editor; use editor::Editor;
use feature_flags::FeatureFlagAppExt; use feature_flags::FeatureFlagAppExt;
use feature_flags::FeatureFlagViewExt; use feature_flags::FeatureFlagViewExt;
use gpui::AsyncWindowContext;
use gpui::Subscription; use gpui::Subscription;
use gpui::Task; use gpui::Task;
use gpui::WeakView; use gpui::WeakView;
@ -312,91 +314,58 @@ impl DevServerProjects {
}); });
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let store = dev_server_projects::Store::global(cx);
cx.spawn({ cx.spawn({
let access_token = access_token.clone();
|this, mut cx| async move { |this, mut cx| async move {
let result = dev_server.await; let result = dev_server.await;
match result { match result {
Ok(dev_server) => { Ok(dev_server) => {
if let Some(ssh_connection_string) = ssh_connection_string { if let Some(ssh_connection_string) = ssh_connection_string {
spawn_ssh_task(
let access_token = access_token.clone(); workspace
this.update(&mut cx, |this, cx| { .upgrade()
this.focus_handle.focus(cx); .ok_or_else(|| anyhow!("workspace dropped"))?,
this.mode = Mode::CreateDevServer(CreateDevServer { store,
creating: true, DevServerId(dev_server.dev_server_id),
dev_server_id: Some(DevServerId(dev_server.dev_server_id)), ssh_connection_string,
access_token: Some(access_token.unwrap_or(dev_server.access_token.clone())), dev_server.access_token.clone(),
manual_setup: false, &mut cx,
});
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,
) )
})?.await?; .await
.log_err();
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
} }
}
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.focus_handle.focus(cx); this.focus_handle.focus(cx);
this.mode = Mode::CreateDevServer(CreateDevServer { this.mode = Mode::CreateDevServer(CreateDevServer {
creating: false, creating: false,
dev_server_id: Some(DevServerId(dev_server.dev_server_id)), dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
access_token: Some(dev_server.access_token), access_token: Some(dev_server.access_token),
manual_setup, manual_setup,
}); });
cx.notify(); cx.notify();
})?; })?;
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer { creating:false, dev_server_id: existing_id, access_token: None, manual_setup }); this.mode = Mode::CreateDevServer(CreateDevServer {
cx.notify() creating: false,
}) dev_server_id: existing_id,
.log_err(); 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); .detach_and_prompt_err("Failed to create server", cx, |_, _| None);
self.mode = Mode::CreateDevServer(CreateDevServer { 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(())
}

View File

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