Fix usability issues with ssh connection modal (#14917)

Release Notes:

- N/A
This commit is contained in:
Max Brunsfeld 2024-07-21 08:43:59 -07:00 committed by GitHub
parent 7b88fc5cda
commit e8bcc412b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 106 additions and 110 deletions

View file

@ -100,18 +100,18 @@ impl SshSession {
) -> Result<Arc<Self>> { ) -> Result<Arc<Self>> {
let client_state = SshClientState::new(user, host, port, delegate.clone(), cx).await?; let client_state = SshClientState::new(user, host, port, delegate.clone(), cx).await?;
let platform = query_platform(&client_state).await?; let platform = client_state.query_platform().await?;
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??; let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
let remote_binary_path = delegate.remote_server_binary_path(cx)?; let remote_binary_path = delegate.remote_server_binary_path(cx)?;
ensure_server_binary( client_state
&client_state, .ensure_server_binary(
&delegate, &delegate,
&local_binary_path, &local_binary_path,
&remote_binary_path, &remote_binary_path,
version, version,
cx, cx,
) )
.await?; .await?;
let (spawn_process_tx, mut spawn_process_rx) = mpsc::unbounded::<SpawnRequest>(); let (spawn_process_tx, mut spawn_process_rx) = mpsc::unbounded::<SpawnRequest>();
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>(); let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
@ -416,7 +416,7 @@ impl SshClientState {
host: String, host: String,
port: u16, port: u16,
delegate: Arc<dyn SshClientDelegate>, delegate: Arc<dyn SshClientDelegate>,
cx: &AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<Self> { ) -> Result<Self> {
Err(anyhow!("ssh is not supported on this platform")) Err(anyhow!("ssh is not supported on this platform"))
} }
@ -427,12 +427,14 @@ impl SshClientState {
host: String, host: String,
port: u16, port: u16,
delegate: Arc<dyn SshClientDelegate>, delegate: Arc<dyn SshClientDelegate>,
cx: &AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<Self> { ) -> Result<Self> {
use futures::{io::BufReader, AsyncBufReadExt as _}; use futures::{io::BufReader, AsyncBufReadExt as _};
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener}; use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
use util::ResultExt as _; use util::ResultExt as _;
delegate.set_status(Some("connecting"), cx);
let url = format!("{user}@{host}"); let url = format!("{user}@{host}");
let temp_dir = tempfile::Builder::new() let temp_dir = tempfile::Builder::new()
.prefix("zed-ssh-session") .prefix("zed-ssh-session")
@ -516,6 +518,79 @@ impl SshClientState {
}) })
} }
async fn ensure_server_binary(
&self,
delegate: &Arc<dyn SshClientDelegate>,
src_path: &Path,
dst_path: &Path,
version: SemanticVersion,
cx: &mut AsyncAppContext,
) -> Result<()> {
let mut dst_path_gz = dst_path.to_path_buf();
dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() {
run_cmd(self.ssh_command("mkdir").arg("-p").arg(parent)).await?;
}
let mut server_binary_exists = false;
if let Ok(installed_version) = run_cmd(self.ssh_command(&dst_path).arg("version")).await {
if installed_version.trim() == version.to_string() {
server_binary_exists = true;
}
}
if server_binary_exists {
log::info!("remote development server already present",);
return Ok(());
}
let src_stat = fs::metadata(src_path).await?;
let size = src_stat.len();
let server_mode = 0o755;
let t0 = Instant::now();
delegate.set_status(Some("uploading remote development server"), cx);
log::info!("uploading remote development server ({}kb)", size / 1024);
self.upload_file(src_path, &dst_path_gz)
.await
.context("failed to upload server binary")?;
log::info!("uploaded remote development server in {:?}", t0.elapsed());
delegate.set_status(Some("extracting remote development server"), cx);
run_cmd(self.ssh_command("gunzip").arg("--force").arg(&dst_path_gz)).await?;
delegate.set_status(Some("unzipping remote development server"), cx);
run_cmd(
self.ssh_command("chmod")
.arg(format!("{:o}", server_mode))
.arg(&dst_path),
)
.await?;
Ok(())
}
async fn query_platform(&self) -> Result<SshPlatform> {
let os = run_cmd(self.ssh_command("uname").arg("-s")).await?;
let arch = run_cmd(self.ssh_command("uname").arg("-m")).await?;
let os = match os.trim() {
"Darwin" => "macos",
"Linux" => "linux",
_ => Err(anyhow!("unknown uname os {os:?}"))?,
};
let arch = if arch.starts_with("arm") || arch.starts_with("aarch64") {
"aarch64"
} else if arch.starts_with("x86") || arch.starts_with("i686") {
"x86_64"
} else {
Err(anyhow!("unknown uname architecture {arch:?}"))?
};
Ok(SshPlatform { os, arch })
}
async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> { async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> {
let mut command = process::Command::new("scp"); let mut command = process::Command::new("scp");
let output = self let output = self
@ -570,84 +645,3 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
)) ))
} }
} }
async fn query_platform(session: &SshClientState) -> Result<SshPlatform> {
let os = run_cmd(session.ssh_command("uname").arg("-s")).await?;
let arch = run_cmd(session.ssh_command("uname").arg("-m")).await?;
let os = match os.trim() {
"Darwin" => "macos",
"Linux" => "linux",
_ => Err(anyhow!("unknown uname os {os:?}"))?,
};
let arch = if arch.starts_with("arm") || arch.starts_with("aarch64") {
"aarch64"
} else if arch.starts_with("x86") || arch.starts_with("i686") {
"x86_64"
} else {
Err(anyhow!("unknown uname architecture {arch:?}"))?
};
Ok(SshPlatform { os, arch })
}
async fn ensure_server_binary(
session: &SshClientState,
delegate: &Arc<dyn SshClientDelegate>,
src_path: &Path,
dst_path: &Path,
version: SemanticVersion,
cx: &mut AsyncAppContext,
) -> Result<()> {
let mut dst_path_gz = dst_path.to_path_buf();
dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() {
run_cmd(session.ssh_command("mkdir").arg("-p").arg(parent)).await?;
}
let mut server_binary_exists = false;
if let Ok(installed_version) = run_cmd(session.ssh_command(&dst_path).arg("version")).await {
if installed_version.trim() == version.to_string() {
server_binary_exists = true;
}
}
if server_binary_exists {
log::info!("remote development server already present",);
return Ok(());
}
let src_stat = fs::metadata(src_path).await?;
let size = src_stat.len();
let server_mode = 0o755;
let t0 = Instant::now();
delegate.set_status(Some("uploading remote development server"), cx);
log::info!("uploading remote development server ({}kb)", size / 1024);
session
.upload_file(src_path, &dst_path_gz)
.await
.context("failed to upload server binary")?;
log::info!("uploaded remote development server in {:?}", t0.elapsed());
delegate.set_status(Some("extracting remote development server"), cx);
run_cmd(
session
.ssh_command("gunzip")
.arg("--force")
.arg(&dst_path_gz),
)
.await?;
delegate.set_status(Some("unzipping remote development server"), cx);
run_cmd(
session
.ssh_command("chmod")
.arg(format!("{:o}", server_mode))
.arg(&dst_path),
)
.await?;
Ok(())
}

View file

@ -12,7 +12,7 @@ use workspace::ModalView;
pub struct SshConnectionModal { pub struct SshConnectionModal {
host: SharedString, host: SharedString,
status: Option<SharedString>, status_message: Option<SharedString>,
prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>, prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>,
editor: View<Editor>, editor: View<Editor>,
} }
@ -22,12 +22,8 @@ impl SshConnectionModal {
Self { Self {
host: host.into(), host: host.into(),
prompt: None, prompt: None,
status: None, status_message: None,
editor: cx.new_view(|cx| { editor: cx.new_view(|cx| Editor::single_line(cx)),
let mut editor = Editor::single_line(cx);
editor.set_redact_all(true, cx);
editor
}),
} }
} }
@ -37,29 +33,35 @@ impl SshConnectionModal {
tx: oneshot::Sender<Result<String>>, tx: oneshot::Sender<Result<String>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.editor.update(cx, |editor, cx| {
if prompt.contains("yes/no") {
editor.set_redact_all(false, cx);
} else {
editor.set_redact_all(true, cx);
}
});
self.prompt = Some((prompt.into(), tx)); self.prompt = Some((prompt.into(), tx));
self.status.take(); self.status_message.take();
cx.focus_view(&self.editor); cx.focus_view(&self.editor);
cx.notify(); cx.notify();
} }
pub fn set_status(&mut self, status: Option<String>, cx: &mut ViewContext<Self>) { pub fn set_status(&mut self, status: Option<String>, cx: &mut ViewContext<Self>) {
self.status = status.map(|s| s.into()); self.status_message = status.map(|s| s.into());
cx.notify(); cx.notify();
} }
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let text = self.editor.read(cx).text(cx);
if let Some((_, tx)) = self.prompt.take() { if let Some((_, tx)) = self.prompt.take() {
tx.send(Ok(text)).ok(); self.editor.update(cx, |editor, cx| {
}; tx.send(Ok(editor.text(cx))).ok();
// cx.emit(DismissEvent) editor.clear(cx);
});
}
} }
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
if self.prompt.is_some() { cx.remove_window();
cx.emit(DismissEvent)
}
} }
} }
@ -74,7 +76,7 @@ impl Render for SshConnectionModal {
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.w(px(400.)) .w(px(400.))
.child(Label::new(format!("SSH: {}", self.host)).size(ui::LabelSize::Large)) .child(Label::new(format!("SSH: {}", self.host)).size(ui::LabelSize::Large))
.when_some(self.status.as_ref(), |el, status| { .when_some(self.status_message.as_ref(), |el, status| {
el.child(Label::new(status.clone())) el.child(Label::new(status.clone()))
}) })
.when_some(self.prompt.as_ref(), |el, prompt| { .when_some(self.prompt.as_ref(), |el, prompt| {