mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-28 09:54:33 +00:00
reconnect ssh (#12147)
Release Notes: - N/A --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
ea166f0b27
commit
af8641ce5b
4 changed files with 230 additions and 125 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -8067,6 +8067,7 @@ name = "recent_projects"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"dev_server_projects",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
|
|
|
@ -14,6 +14,7 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fuzzy.workspace = true
|
||||
|
|
|
@ -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::<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,
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
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<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,
|
||||
|
|
Loading…
Reference in a new issue