Compare commits

...

10 Commits

Author SHA1 Message Date
Zed Bot
460b4d49e1 Bump to 0.160.3 for @ConradIrwin 2024-11-01 20:28:44 +00:00
Conrad Irwin
c4a0c9acca zed 0.160.2 2024-10-31 20:28:43 -06:00
Conrad Irwin
8ab5b67a4d SSHHELL escaping.... (#20046)
Closes #20027 
Closes #19976 (again)

Release Notes:

- Remoting: Fixed remotes with non-sh/bash/zsh default shells
- Remoting: Fixed remotes running busybox's version of gunzip
2024-10-31 16:13:02 -06:00
Conrad Irwin
18c1268b25 SSH installation refactor (#19991)
This also cleans up logic for deciding how to do things.

Release Notes:

- Remoting: If downloading the binary on the remote fails, fall back to
uploading it.

---------

Co-authored-by: Mikayala <mikayla@zed.dev>
2024-10-31 16:12:58 -06:00
Peter Tripp
0cb056c26a zed 0.160.1 2024-10-31 07:42:46 -04:00
Kyle Kelley
68a683ab4b Trim whitespace from base64 encoded image data before decoding it (#19977)
Closes #17956
Closes #16330

This fix is for both REPL (released) and notebook (unreleased)

<img width="1210" alt="image"
src="https://github.com/user-attachments/assets/bd046f0f-3ad1-4c25-b3cb-114e008c2a69">

Release Notes:

- Fixed image support in REPL for certain versions of matplotlib that
included preceding and/or trailing whitespace in the base64 image data
2024-10-31 07:42:09 -04:00
Conrad Irwin
4abd65e894 Robustify download on remote (#19983)
Closes #19976
Closes #19972

We now prefer curl to wget (as it supports socks5:// proxies) and pass
-f to
curl so it fails; and use sh instead of bash, which should have more
consistent
behaviour across systems

Release Notes:

- SSH Remoting: make downloading binary on remote more reliable.

---------

Co-authored-by: Will <will@zed.dev>
2024-10-31 07:42:04 -04:00
Conrad Irwin
e2155a9e40 Fail download if download fails (#19990)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- Remoting: Fixes a bug where we could cache an HTML error page as a
binary

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-10-31 07:42:00 -04:00
Conrad Irwin
f862fe4eb7 Support zed://ssh (#19970)
Closes: #15070

Release Notes:

- Added support for `zed://ssh/<connnection>/<path>`
2024-10-30 11:31:32 -06:00
Peter Tripp
8560f0dc06 v0.160.x preview 2024-10-30 11:06:24 -04:00
10 changed files with 434 additions and 371 deletions

4
Cargo.lock generated
View File

@@ -9533,9 +9533,11 @@ dependencies = [
"fs",
"futures 0.3.30",
"gpui",
"itertools 0.13.0",
"log",
"parking_lot",
"prost",
"release_channel",
"rpc",
"serde",
"serde_json",
@@ -15033,7 +15035,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.160.0"
version = "0.160.3"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -686,6 +686,12 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;

View File

@@ -13,8 +13,7 @@ use gpui::{AppContext, Model};
use language::CursorShape;
use markdown::{Markdown, MarkdownStyle};
use release_channel::{AppVersion, ReleaseChannel};
use remote::ssh_session::{ServerBinary, ServerVersion};
use release_channel::ReleaseChannel;
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -441,23 +440,66 @@ impl remote::SshClientDelegate for SshClientDelegate {
self.update_status(status, cx)
}
fn get_server_binary(
fn download_server_binary_locally(
&self,
platform: SshPlatform,
upload_binary_over_ssh: bool,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
let (tx, rx) = oneshot::channel();
let this = self.clone();
) -> Task<anyhow::Result<PathBuf>> {
cx.spawn(|mut cx| async move {
tx.send(
this.get_server_binary_impl(platform, upload_binary_over_ssh, &mut cx)
.await,
let binary_path = AutoUpdater::download_remote_server_release(
platform.os,
platform.arch,
release_channel,
version,
&mut cx,
)
.ok();
.await
.map_err(|e| {
anyhow!(
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
version
.map(|v| format!("{}", v))
.unwrap_or("unknown".to_string()),
platform.os,
platform.arch,
e
)
})?;
Ok(binary_path)
})
.detach();
rx
}
fn get_download_params(
&self,
platform: SshPlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Task<Result<(String, String)>> {
cx.spawn(|mut cx| async move {
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
version,
&mut cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
version.map(|v| format!("{}", v)).unwrap_or("unknown".to_string()),
platform.os,
platform.arch,
e
)
})?;
Ok((release.url, request_body))
}
)
}
fn remote_server_binary_path(
@@ -485,208 +527,6 @@ impl SshClientDelegate {
})
.ok();
}
async fn get_server_binary_impl(
&self,
platform: SshPlatform,
upload_binary_via_ssh: bool,
cx: &mut AsyncAppContext,
) -> Result<(ServerBinary, ServerVersion)> {
let (version, release_channel) = cx.update(|cx| {
let version = AppVersion::global(cx);
let channel = ReleaseChannel::global(cx);
(version, channel)
})?;
// In dev mode, build the remote server binary from source
#[cfg(debug_assertions)]
if release_channel == ReleaseChannel::Dev {
let result = self.build_local(cx, platform, version).await?;
// Fall through to a remote binary if we're not able to compile a local binary
if let Some((path, version)) = result {
return Ok((
ServerBinary::LocalBinary(path),
ServerVersion::Semantic(version),
));
}
}
// For nightly channel, always get latest
let current_version = if release_channel == ReleaseChannel::Nightly {
None
} else {
Some(version)
};
self.update_status(
Some(&format!("Checking remote server release {}", version)),
cx,
);
if upload_binary_via_ssh {
let binary_path = AutoUpdater::download_remote_server_release(
platform.os,
platform.arch,
release_channel,
current_version,
cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
version,
platform.os,
platform.arch,
e
)
})?;
Ok((
ServerBinary::LocalBinary(binary_path),
ServerVersion::Semantic(version),
))
} else {
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
current_version,
cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
version,
platform.os,
platform.arch,
e
)
})?;
let version = release
.version
.parse::<SemanticVersion>()
.map(ServerVersion::Semantic)
.unwrap_or_else(|_| ServerVersion::Commit(release.version));
Ok((
ServerBinary::ReleaseUrl {
url: release.url,
body: request_body,
},
version,
))
}
}
#[cfg(debug_assertions)]
async fn build_local(
&self,
cx: &mut AsyncAppContext,
platform: SshPlatform,
version: gpui::SemanticVersion,
) -> Result<Option<(PathBuf, gpui::SemanticVersion)>> {
use smol::process::{Command, Stdio};
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stderr(Stdio::inherit())
.output()
.await?;
if !output.status.success() {
Err(anyhow!("Failed to run command: {:?}", command))?;
}
Ok(())
}
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
self.update_status(Some("Building remote server binary from source"), cx);
log::info!("building remote server binary from source");
run_cmd(Command::new("cargo").args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
]))
.await?;
self.update_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
"target/remote_server/debug/remote_server",
]))
.await?;
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
return Ok(Some((path, version)));
} else if let Some(triple) = platform.triple() {
smol::fs::create_dir_all("target/remote_server").await?;
self.update_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross");
run_cmd(Command::new("cargo").args([
"install",
"cross",
"--git",
"https://github.com/cross-rs/cross",
]))
.await?;
self.update_status(
Some(&format!(
"Building remote server binary from source for {} with Docker",
&triple
)),
cx,
);
log::info!("building remote server binary from source for {}", &triple);
run_cmd(
Command::new("cross")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env(
"CROSS_CONTAINER_OPTS",
"--mount type=bind,src=./target,dst=/app/target",
),
)
.await?;
self.update_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
&format!("target/remote_server/{}/debug/remote_server", triple),
]))
.await?;
let path = std::env::current_dir()?.join(format!(
"target/remote_server/{}/debug/remote_server.gz",
triple
));
return Ok(Some((path, version)));
} else {
return Ok(None);
}
}
}
pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {

View File

@@ -24,6 +24,7 @@ collections.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
log.workspace = true
parking_lot.workspace = true
prost.workspace = true
@@ -35,6 +36,7 @@ smol.workspace = true
tempfile.workspace = true
thiserror.workspace = true
util.workspace = true
release_channel.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -20,7 +20,9 @@ use gpui::{
AppContext, AsyncAppContext, BorrowAppContext, Context, EventEmitter, Global, Model,
ModelContext, SemanticVersion, Task, WeakModel,
};
use itertools::Itertools;
use parking_lot::Mutex;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use rpc::{
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet,
@@ -33,8 +35,7 @@ use smol::{
use std::{
any::TypeId,
collections::VecDeque,
ffi::OsStr,
fmt,
fmt, iter,
ops::ControlFlow,
path::{Path, PathBuf},
sync::{
@@ -69,6 +70,18 @@ pub struct SshConnectionOptions {
pub upload_binary_over_ssh: bool,
}
#[macro_export]
macro_rules! shell_script {
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
format!(
$fmt,
$(
$name = shlex::try_quote($arg).unwrap()
),+
)
}};
}
impl SshConnectionOptions {
pub fn parse_command_line(input: &str) -> Result<Self> {
let input = input.trim_start_matches("ssh ");
@@ -227,10 +240,19 @@ pub enum ServerBinary {
ReleaseUrl { url: String, body: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerVersion {
Semantic(SemanticVersion),
Commit(String),
}
impl ServerVersion {
pub fn semantic_version(&self) -> Option<SemanticVersion> {
match self {
Self::Semantic(version) => Some(*version),
_ => None,
}
}
}
impl std::fmt::Display for ServerVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -252,24 +274,45 @@ pub trait SshClientDelegate: Send + Sync {
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<PathBuf>;
fn get_server_binary(
fn get_download_params(
&self,
platform: SshPlatform,
upload_binary_over_ssh: bool,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>>;
) -> Task<Result<(String, String)>>;
fn download_server_binary_locally(
&self,
platform: SshPlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Task<Result<PathBuf>>;
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
}
impl SshSocket {
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
// :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
// e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
// and passes -l as an argument to sh, not to ls.
// You need to do it like this: $ ssh host "sh -c 'ls -l /tmp'"
fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command {
let mut command = process::Command::new("ssh");
let to_run = iter::once(&program)
.chain(args.iter())
.map(|token| shlex::try_quote(token).unwrap())
.join(" ");
self.ssh_options(&mut command)
.arg(self.connection_options.ssh_url())
.arg(program);
.arg(to_run);
command
}
fn shell_script(&self, script: impl AsRef<str>) -> process::Command {
return self.ssh_command("sh", &["-c", script.as_ref()]);
}
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
command
.stdin(Stdio::piped())
@@ -290,7 +333,7 @@ impl SshSocket {
}
}
async fn run_cmd(command: &mut process::Command) -> Result<String> {
async fn run_cmd(mut command: process::Command) -> Result<String> {
let output = command.output().await?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
@@ -1217,7 +1260,7 @@ impl RemoteConnection for SshRemoteConnection {
}
let socket = self.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
run_cmd(socket.ssh_command(&remote_binary_path.to_string_lossy(), &["version"])).await?;
Ok(remote_binary_path)
}
@@ -1234,22 +1277,33 @@ impl RemoteConnection for SshRemoteConnection {
) -> Task<Result<i32>> {
delegate.set_status(Some("Starting proxy"), cx);
let mut start_proxy_command = format!(
"RUST_LOG={} {} {:?} proxy --identifier {}",
std::env::var("RUST_LOG").unwrap_or_default(),
std::env::var("RUST_BACKTRACE")
.map(|b| { format!("RUST_BACKTRACE={}", b) })
.unwrap_or_default(),
remote_binary_path,
unique_identifier,
let mut start_proxy_command = shell_script!(
"exec {binary_path} proxy --identifier {identifier}",
binary_path = &remote_binary_path.to_string_lossy(),
identifier = &unique_identifier,
);
if let Some(rust_log) = std::env::var("RUST_LOG").ok() {
start_proxy_command = format!(
"RUST_LOG={} {}",
shlex::try_quote(&rust_log).unwrap(),
start_proxy_command
)
}
if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() {
start_proxy_command = format!(
"RUST_BACKTRACE={} {}",
shlex::try_quote(&rust_backtrace).unwrap(),
start_proxy_command
)
}
if reconnect {
start_proxy_command.push_str(" --reconnect");
}
let ssh_proxy_process = match self
.socket
.ssh_command(start_proxy_command)
.shell_script(start_proxy_command)
// IMPORTANT: we kill this process when we drop the task that uses it.
.kill_on_drop(true)
.spawn()
@@ -1431,8 +1485,8 @@ impl SshRemoteConnection {
socket_path,
};
let os = run_cmd(socket.ssh_command("uname").arg("-s")).await?;
let arch = run_cmd(socket.ssh_command("uname").arg("-m")).await?;
let os = run_cmd(socket.ssh_command("uname", &["-s"])).await?;
let arch = run_cmd(socket.ssh_command("uname", &["-m"])).await?;
let os = match os.trim() {
"Darwin" => "macos",
@@ -1630,14 +1684,9 @@ impl SshRemoteConnection {
}
async fn get_ssh_source_port(&self) -> Result<String> {
let output = run_cmd(
self.socket
.ssh_command("sh")
.arg("-c")
.arg(r#""echo $SSH_CLIENT | cut -d' ' -f2""#),
)
.await
.context("failed to get source port from SSH_CLIENT on host")?;
let output = run_cmd(self.socket.shell_script("echo $SSH_CLIENT | cut -d' ' -f2"))
.await
.context("failed to get source port from SSH_CLIENT on host")?;
Ok(output.trim().to_string())
}
@@ -1648,13 +1697,13 @@ impl SshRemoteConnection {
.ok_or_else(|| anyhow!("Lock file path has no parent directory"))?;
let script = format!(
r#"'mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists"'"#,
r#"mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists""#,
parent_dir = parent_dir.display(),
lock_file = lock_file.display(),
content = content,
);
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script))
let output = run_cmd(self.socket.shell_script(&script))
.await
.with_context(|| format!("failed to create a lock file at {:?}", lock_file))?;
@@ -1662,7 +1711,7 @@ impl SshRemoteConnection {
}
fn generate_stale_check_script(lock_file: &Path, max_age: u64) -> String {
format!(
shell_script!(
r#"
if [ ! -f "{lock_file}" ]; then
echo "lock file does not exist"
@@ -1690,18 +1739,15 @@ impl SshRemoteConnection {
else
echo "recent"
fi"#,
lock_file = lock_file.display(),
max_age = max_age
lock_file = &lock_file.to_string_lossy(),
max_age = &max_age.to_string()
)
}
async fn is_lock_stale(&self, lock_file: &Path, max_age: &Duration) -> Result<bool> {
let script = format!(
"'{}'",
Self::generate_stale_check_script(lock_file, max_age.as_secs())
);
let script = Self::generate_stale_check_script(lock_file, max_age.as_secs());
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script))
let output = run_cmd(self.socket.shell_script(script))
.await
.with_context(|| {
format!("failed to check whether lock file {:?} is stale", lock_file)
@@ -1714,9 +1760,12 @@ impl SshRemoteConnection {
}
async fn remove_lock_file(&self, lock_file: &Path) -> Result<()> {
run_cmd(self.socket.ssh_command("rm").arg("-f").arg(lock_file))
.await
.context("failed to remove lock file")?;
run_cmd(
self.socket
.ssh_command("rm", &["-f", &lock_file.to_string_lossy()]),
)
.await
.context("failed to remove lock file")?;
Ok(())
}
@@ -1727,109 +1776,149 @@ impl SshRemoteConnection {
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<()> {
if std::env::var("ZED_USE_CACHED_REMOTE_SERVER").is_ok() {
if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
log::info!("using cached server binary version {}", installed_version);
return Ok(());
}
}
if cfg!(not(debug_assertions)) {
// When we're not in dev mode, we don't want to switch out the binary if it's
// still open.
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
// to still replace the binary.
if self.is_binary_in_use(dst_path).await? {
log::info!("server binary is opened by another process. not updating");
delegate.set_status(
Some("Skipping update of remote development server, since it's still in use"),
cx,
);
return Ok(());
}
}
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
let (binary, new_server_version) = delegate
.get_server_binary(platform, upload_binary_over_ssh, cx)
.await??;
if cfg!(not(debug_assertions)) {
let installed_version = if let Ok(version_output) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
let current_version = match run_cmd(
self.socket
.ssh_command(&dst_path.to_string_lossy(), &["version"]),
)
.await
{
Ok(version_output) => {
if let Ok(version) = version_output.trim().parse::<SemanticVersion>() {
Some(ServerVersion::Semantic(version))
} else {
Some(ServerVersion::Commit(version_output.trim().to_string()))
}
} else {
None
}
Err(_) => None,
};
let (release_channel, wanted_version) = cx.update(|cx| {
let release_channel = ReleaseChannel::global(cx);
let wanted_version = match release_channel {
ReleaseChannel::Nightly => {
AppCommitSha::try_global(cx).map(|sha| ServerVersion::Commit(sha.0))
}
ReleaseChannel::Dev => None,
_ => Some(ServerVersion::Semantic(AppVersion::global(cx))),
};
(release_channel, wanted_version)
})?;
if let Some(installed_version) = installed_version {
use ServerVersion::*;
match (installed_version, new_server_version) {
(Semantic(installed), Semantic(new)) if installed == new => {
log::info!("remote development server present and matching client version");
return Ok(());
}
(Semantic(installed), Semantic(new)) if installed > new => {
let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", installed, new);
return Err(error);
}
(Commit(installed), Commit(new)) if installed == new => {
log::info!(
"remote development server present and matching client version {}",
installed
);
return Ok(());
}
(installed, _) => {
log::info!(
"remote development server has version: {}. updating...",
installed
);
}
match (&current_version, &wanted_version) {
(Some(current), Some(wanted)) if current == wanted => {
log::info!("remote development server present and matching client version");
return Ok(());
}
(Some(ServerVersion::Semantic(current)), Some(ServerVersion::Semantic(wanted)))
if current > wanted =>
{
anyhow::bail!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", current, wanted);
}
_ => {
log::info!("Installing remote development server");
}
}
if self.is_binary_in_use(dst_path).await? {
// When we're not in dev mode, we don't want to switch out the binary if it's
// still open.
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
// to still replace the binary.
if cfg!(not(debug_assertions)) {
anyhow::bail!("The remote server version ({:?}) does not match the wanted version ({:?}), but is in use by another Zed client so cannot be upgraded.", &current_version, &wanted_version)
} else {
log::info!("Binary is currently in use, ignoring because this is a dev build")
}
}
if wanted_version.is_none() {
if std::env::var("ZED_BUILD_REMOTE_SERVER").is_err() {
if let Some(current_version) = current_version {
log::warn!(
"In development, using cached remote server binary version ({})",
current_version
);
return Ok(());
} else {
anyhow::bail!(
"ZED_BUILD_REMOTE_SERVER is not set, but no remote server exists at ({:?})",
dst_path
)
}
}
#[cfg(debug_assertions)]
{
let src_path = self.build_local(platform, delegate, cx).await?;
return self
.upload_local_server_binary(&src_path, dst_path, delegate, cx)
.await;
}
#[cfg(not(debug_assertions))]
anyhow::bail!("Running development build in release mode, cannot cross compile (unset ZED_BUILD_REMOTE_SERVER)")
}
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
if !upload_binary_over_ssh {
let (url, body) = delegate
.get_download_params(
platform,
release_channel,
wanted_version.clone().and_then(|v| v.semantic_version()),
cx,
)
.await?;
match self
.download_binary_on_server(&url, &body, dst_path, delegate, cx)
.await
{
Ok(_) => return Ok(()),
Err(e) => {
log::error!(
"Failed to download binary on server, attempting to upload server: {}",
e
)
}
}
}
match binary {
ServerBinary::LocalBinary(src_path) => {
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
.await
}
ServerBinary::ReleaseUrl { url, body } => {
self.download_binary_on_server(&url, &body, dst_path, delegate, cx)
.await
}
}
let src_path = delegate
.download_server_binary_locally(
platform,
release_channel,
wanted_version.and_then(|v| v.semantic_version()),
cx,
)
.await?;
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
.await
}
async fn is_binary_in_use(&self, binary_path: &Path) -> Result<bool> {
let script = format!(
r#"'
let script = shell_script!(
r#"
if command -v lsof >/dev/null 2>&1; then
if lsof "{}" >/dev/null 2>&1; then
if lsof "{binary_path}" >/dev/null 2>&1; then
echo "in_use"
exit 0
fi
elif command -v fuser >/dev/null 2>&1; then
if fuser "{}" >/dev/null 2>&1; then
if fuser "{binary_path}" >/dev/null 2>&1; then
echo "in_use"
exit 0
fi
fi
echo "not_in_use"
'"#,
binary_path.display(),
binary_path.display(),
"#,
binary_path = &binary_path.to_string_lossy(),
);
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script))
let output = run_cmd(self.socket.shell_script(script))
.await
.context("failed to check if binary is in use")?;
@@ -1848,31 +1937,32 @@ impl SshRemoteConnection {
dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() {
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
run_cmd(
self.socket
.ssh_command("mkdir", &["-p", &parent.to_string_lossy()]),
)
.await?;
}
delegate.set_status(Some("Downloading remote development server on host"), cx);
let script = format!(
let script = shell_script!(
r#"
if command -v wget >/dev/null 2>&1; then
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data='{}' '{}' -O '{}' && echo "wget"
elif command -v curl >/dev/null 2>&1; then
curl -L -X GET -H "Content-Type: application/json" -d '{}' '{}' -o '{}' && echo "curl"
if command -v curl >/dev/null 2>&1; then
curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_path} && echo "curl"
elif command -v wget >/dev/null 2>&1; then
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_path} && echo "wget"
else
echo "Neither curl nor wget is available" >&2
exit 1
fi
"#,
body.replace("'", r#"\'"#),
url,
dst_path_gz.display(),
body.replace("'", r#"\'"#),
url,
dst_path_gz.display(),
body = body,
url = url,
dst_path = &dst_path_gz.to_string_lossy(),
);
let output = run_cmd(self.socket.ssh_command("bash").arg("-c").arg(script))
let output = run_cmd(self.socket.shell_script(script))
.await
.context("Failed to download server binary")?;
@@ -1895,7 +1985,11 @@ impl SshRemoteConnection {
dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() {
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
run_cmd(
self.socket
.ssh_command("mkdir", &["-p", &parent.to_string_lossy()]),
)
.await?;
}
let src_stat = fs::metadata(&src_path).await?;
@@ -1923,20 +2017,16 @@ impl SshRemoteConnection {
delegate.set_status(Some("Extracting remote development server"), cx);
run_cmd(
self.socket
.ssh_command("gunzip")
.arg("--force")
.arg(&dst_path_gz),
.ssh_command("gunzip", &["-f", &dst_path_gz.to_string_lossy()]),
)
.await?;
let server_mode = 0o755;
delegate.set_status(Some("Marking remote development server executable"), cx);
run_cmd(
self.socket
.ssh_command("chmod")
.arg(format!("{:o}", server_mode))
.arg(dst_path),
)
run_cmd(self.socket.ssh_command(
"chmod",
&[&format!("{:o}", server_mode), &dst_path.to_string_lossy()],
))
.await?;
Ok(())
@@ -1974,6 +2064,113 @@ impl SshRemoteConnection {
))
}
}
#[cfg(debug_assertions)]
async fn build_local(
&self,
platform: SshPlatform,
delegate: &Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
) -> Result<PathBuf> {
use smol::process::{Command, Stdio};
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stderr(Stdio::inherit())
.output()
.await?;
if !output.status.success() {
Err(anyhow!("Failed to run command: {:?}", command))?;
}
Ok(())
}
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
delegate.set_status(Some("Building remote server binary from source"), cx);
log::info!("building remote server binary from source");
run_cmd(Command::new("cargo").args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
]))
.await?;
delegate.set_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
"target/remote_server/debug/remote_server",
]))
.await?;
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
return Ok(path);
}
let Some(triple) = platform.triple() else {
anyhow::bail!("can't cross compile for: {:?}", platform);
};
smol::fs::create_dir_all("target/remote_server").await?;
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross");
run_cmd(Command::new("cargo").args([
"install",
"cross",
"--git",
"https://github.com/cross-rs/cross",
]))
.await?;
delegate.set_status(
Some(&format!(
"Building remote server binary from source for {} with Docker",
&triple
)),
cx,
);
log::info!("building remote server binary from source for {}", &triple);
run_cmd(
Command::new("cross")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env(
"CROSS_CONTAINER_OPTS",
"--mount type=bind,src=./target,dst=/app/target",
),
)
.await?;
delegate.set_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
&format!("target/remote_server/{}/debug/remote_server", triple),
]))
.await?;
let path = std::env::current_dir()?.join(format!(
"target/remote_server/{}/debug/remote_server.gz",
triple
));
return Ok(path);
}
}
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
@@ -2295,12 +2492,12 @@ mod fake {
},
select_biased, FutureExt, SinkExt, StreamExt,
};
use gpui::{AsyncAppContext, Task, TestAppContext};
use gpui::{AsyncAppContext, SemanticVersion, Task, TestAppContext};
use release_channel::ReleaseChannel;
use rpc::proto::Envelope;
use super::{
ChannelClient, RemoteConnection, ServerBinary, ServerVersion, SshClientDelegate,
SshConnectionOptions, SshPlatform,
ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform,
};
pub(super) struct FakeRemoteConnection {
@@ -2412,23 +2609,36 @@ mod fake {
) -> oneshot::Receiver<Result<String>> {
unreachable!()
}
fn remote_server_binary_path(
fn download_server_binary_locally(
&self,
_: SshPlatform,
_: ReleaseChannel,
_: Option<SemanticVersion>,
_: &mut AsyncAppContext,
) -> Result<PathBuf> {
) -> Task<Result<PathBuf>> {
unreachable!()
}
fn get_server_binary(
fn get_download_params(
&self,
_: SshPlatform,
_: bool,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
_platform: SshPlatform,
_release_channel: ReleaseChannel,
_version: Option<SemanticVersion>,
_cx: &mut AsyncAppContext,
) -> Task<Result<(String, String)>> {
unreachable!()
}
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {}
fn remote_server_binary_path(
&self,
_platform: SshPlatform,
_cx: &mut AsyncAppContext,
) -> Result<PathBuf> {
unreachable!()
}
}
}

View File

@@ -16,7 +16,7 @@ pub struct ImageView {
impl ImageView {
pub fn from(base64_encoded_data: &str) -> Result<Self> {
let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
let bytes = BASE64_STANDARD.decode(base64_encoded_data.trim())?;
let format = image::guess_format(&bytes)?;
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.160.0"
version = "0.160.3"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -47,6 +47,9 @@ impl OpenRequest {
this.parse_file_path(file)
} else if let Some(file) = url.strip_prefix("zed://file") {
this.parse_file_path(file)
} else if let Some(file) = url.strip_prefix("zed://ssh") {
let ssh_url = "ssh:/".to_string() + file;
this.parse_ssh_file_path(&ssh_url, cx)?
} else if url.starts_with("ssh://") {
this.parse_ssh_file_path(&url, cx)?
} else if let Some(request_path) = parse_zed_link(&url, cx) {

View File

@@ -23,7 +23,7 @@ On your local machine, Zed runs its UI, talks to language models, uses Tree-sitt
1. Once the Zed server is running, you will be prompted to choose a path to open on the remote server.
> **Note:** Zed does not currently handle opening very large directories (for example, `/` or `~` that may have >100,000 files) very well. We are working on improving this, but suggest in the meantime opening only specific projects, or subfolders of very large mono-repos.
For simple cases where you don't need any SSH arguments, you can run `zed ssh://[<user>@]<host>[:<port>]/<path>` to open a remote folder/file directly.
For simple cases where you don't need any SSH arguments, you can run `zed ssh://[<user>@]<host>[:<port>]/<path>` to open a remote folder/file directly. If you'd like to hotlink into an SSH project, use a link of the format: `zed://ssh/[<user>@]<host>[:<port>]/<path>`.
## Supported platforms