From f78f3e7729b6e505685ba20ef207c709f0229149 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Aug 2025 17:18:52 -0700 Subject: [PATCH] Add initial support for WSL (#37035) Closes #36188 ## Todo * [x] CLI * [x] terminals * [x] tasks ## For future PRs * debugging * UI for opening WSL projects * fixing workspace state restoration Release Notes: - Windows alpha: Zed now supports editing folders in WSL. --------- Co-authored-by: Junkui Zhang <364772080@qq.com> --- crates/auto_update_helper/src/updater.rs | 26 +- crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 74 ++- crates/extension_host/src/extension_host.rs | 11 +- crates/paths/src/paths.rs | 5 + crates/project/src/debugger/dap_store.rs | 10 +- crates/project/src/project.rs | 4 +- crates/project/src/terminals.rs | 2 +- .../src/disconnected_overlay.rs | 31 +- crates/recent_projects/src/recent_projects.rs | 33 +- ...h_connections.rs => remote_connections.rs} | 138 ++--- crates/recent_projects/src/remote_servers.rs | 123 +++-- crates/remote/src/remote.rs | 3 +- crates/remote/src/remote_client.rs | 117 ++++- crates/remote/src/transport.rs | 335 ++++++++++++ crates/remote/src/transport/ssh.rs | 341 +----------- crates/remote/src/transport/wsl.rs | 494 ++++++++++++++++++ crates/title_bar/src/title_bar.rs | 13 +- crates/workspace/src/persistence.rs | 438 +++++++++++----- crates/workspace/src/persistence/model.rs | 32 +- crates/workspace/src/workspace.rs | 59 +-- crates/zed/resources/windows/zed-wsl | 25 + crates/zed/src/main.rs | 75 +-- crates/zed/src/zed.rs | 4 +- crates/zed/src/zed/open_listener.rs | 91 ++-- crates/zed/src/zed/windows_only_instance.rs | 1 + script/bundle-windows.ps1 | 1 + 27 files changed, 1701 insertions(+), 786 deletions(-) rename crates/recent_projects/src/{ssh_connections.rs => remote_connections.rs} (85%) create mode 100644 crates/remote/src/transport/wsl.rs create mode 100644 crates/zed/resources/windows/zed-wsl diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 762771617609e63996685d3d96fae69135355249..a48bbccec304a1b49bb0496c21b299f5dd176076 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED; type Job = fn(&Path) -> Result<()>; #[cfg(not(test))] -pub(crate) const JOBS: [Job; 6] = [ +pub(crate) const JOBS: &[Job] = &[ // Delete old files |app_dir| { let zed_executable = app_dir.join("Zed.exe"); @@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [ std::fs::remove_file(&zed_cli) .context(format!("Failed to remove old file {}", zed_cli.display())) }, + |app_dir| { + let zed_wsl = app_dir.join("bin\\zed"); + log::info!("Removing old file: {}", zed_wsl.display()); + std::fs::remove_file(&zed_wsl) + .context(format!("Failed to remove old file {}", zed_wsl.display())) + }, // Copy new files |app_dir| { let zed_executable_source = app_dir.join("install\\Zed.exe"); @@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [ zed_cli_dest.display() )) }, + |app_dir| { + let zed_wsl_source = app_dir.join("install\\bin\\zed"); + let zed_wsl_dest = app_dir.join("bin\\zed"); + log::info!( + "Copying new file {} to {}", + zed_wsl_source.display(), + zed_wsl_dest.display() + ); + std::fs::copy(&zed_wsl_source, &zed_wsl_dest) + .map(|_| ()) + .context(format!( + "Failed to copy new file {} to {}", + zed_wsl_source.display(), + zed_wsl_dest.display() + )) + }, // Clean up installer folder and updates folder |app_dir| { let updates_folder = app_dir.join("updates"); @@ -85,7 +107,7 @@ pub(crate) const JOBS: [Job; 6] = [ ]; #[cfg(test)] -pub(crate) const JOBS: [Job; 2] = [ +pub(crate) const JOBS: &[Job] = &[ |_| { std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 6274f69035a02bed20d1a85608371744395c951a..79a10fa2b0936b44d9500fd9990ffa4c6ac62e85 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -14,6 +14,7 @@ pub enum CliRequest { paths: Vec, urls: Vec, diff_paths: Vec<[String; 2]>, + wsl: Option, wait: bool, open_new_workspace: Option, env: Option>, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index b84e7a9f7a53a471bd854a15377c79f45003aaf4..151e96e3cf68ab94295a8386d2842539e6a986a2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -6,7 +6,6 @@ use anyhow::{Context as _, Result}; use clap::Parser; use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; -use collections::HashMap; use parking_lot::Mutex; use std::{ env, fs, io, @@ -85,6 +84,15 @@ struct Args { /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option, + /// The username and WSL distribution to use when opening paths. ,If not specified, + /// Zed will attempt to open the paths directly. + /// + /// The username is optional, and if not specified, the default user for the distribution + /// will be used. + /// + /// Example: `me@Ubuntu` or `Ubuntu` for default distribution. + #[arg(long, value_name = "USER@DISTRO")] + wsl: Option, /// Not supported in Zed CLI, only supported on Zed binary /// Will attempt to give the correct command to run #[arg(long)] @@ -129,14 +137,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result { Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string())) } -fn main() -> Result<()> { - #[cfg(all(not(debug_assertions), target_os = "windows"))] - unsafe { - use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole}; +fn parse_path_in_wsl(source: &str, wsl: &str) -> Result { + let mut command = util::command::new_std_command("wsl.exe"); - let _ = AttachConsole(ATTACH_PARENT_PROCESS); + let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { + if user.is_empty() { + anyhow::bail!("user is empty in wsl argument"); + } + (Some(user), distro) + } else { + (None, wsl) + }; + + if let Some(user) = user { + command.arg("--user").arg(user); } + let output = command + .arg("--distribution") + .arg(distro_name) + .arg("wslpath") + .arg("-m") + .arg(source) + .output()?; + + let result = String::from_utf8_lossy(&output.stdout); + let prefix = format!("//wsl.localhost/{}", distro_name); + + Ok(result + .trim() + .strip_prefix(&prefix) + .unwrap_or(&result) + .to_string()) +} + +fn main() -> Result<()> { #[cfg(unix)] util::prevent_root_execution(); @@ -223,6 +258,8 @@ fn main() -> Result<()> { let env = { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { + use collections::HashMap; + // On Linux, the desktop entry uses `cli` to spawn `zed`. // We need to handle env vars correctly since std::env::vars() may not contain // project-specific vars (e.g. those set by direnv). @@ -235,8 +272,19 @@ fn main() -> Result<()> { } } - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - Some(std::env::vars().collect::>()) + #[cfg(target_os = "windows")] + { + // On Windows, by default, a child process inherits a copy of the environment block of the parent process. + // So we don't need to pass env vars explicitly. + None + } + + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] + { + use collections::HashMap; + + Some(std::env::vars().collect::>()) + } }; let exit_status = Arc::new(Mutex::new(None)); @@ -271,8 +319,10 @@ fn main() -> Result<()> { paths.push(tmp_file.path().to_string_lossy().to_string()); let (tmp_file, _) = tmp_file.keep()?; anonymous_fd_tmp_files.push((file, tmp_file)); + } else if let Some(wsl) = &args.wsl { + urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?)); } else { - paths.push(parse_path_with_position(path)?) + paths.push(parse_path_with_position(path)?); } } @@ -292,6 +342,7 @@ fn main() -> Result<()> { paths, urls, diff_paths, + wsl: args.wsl, wait: args.wait, open_new_workspace, env, @@ -644,15 +695,15 @@ mod windows { Storage::FileSystem::{ CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile, }, - System::Threading::CreateMutexW, + System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW}, }, core::HSTRING, }; use crate::{Detect, InstalledApp}; - use std::io; use std::path::{Path, PathBuf}; use std::process::ExitStatus; + use std::{io, os::windows::process::CommandExt}; fn check_single_instance() -> bool { let mutex = unsafe { @@ -691,6 +742,7 @@ mod windows { fn launch(&self, ipc_url: String) -> anyhow::Result<()> { if check_single_instance() { std::process::Command::new(self.0.clone()) + .creation_flags(CREATE_NEW_PROCESS_GROUP.0) .arg(ipc_url) .spawn()?; } else { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index b8189c36511a03f136e5e215549453947e888bb1..b114ad9f4c526f9c270681c55626455531becc2f 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -43,7 +43,7 @@ use language::{ use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; -use remote::RemoteClient; +use remote::{RemoteClient, RemoteConnectionOptions}; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -117,7 +117,7 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, - pub remote_clients: HashMap>, + pub remote_clients: HashMap>, pub ssh_registered_tx: UnboundedSender<()>, } @@ -1779,16 +1779,15 @@ impl ExtensionStore { } pub fn register_remote_client(&mut self, client: Entity, cx: &mut Context) { - let connection_options = client.read(cx).connection_options(); - let ssh_url = connection_options.ssh_url(); + let options = client.read(cx).connection_options(); - if let Some(existing_client) = self.remote_clients.get(&ssh_url) + if let Some(existing_client) = self.remote_clients.get(&options) && existing_client.upgrade().is_some() { return; } - self.remote_clients.insert(ssh_url, client.downgrade()); + self.remote_clients.insert(options, client.downgrade()); self.ssh_registered_tx.unbounded_send(()).ok(); } } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index c2c3c89305939bc32c635549c23d64d565f8fbb0..ede42af0272902892afd2e9dfdafb5c5eae2f8f5 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -33,6 +33,11 @@ pub fn remote_server_dir_relative() -> &'static Path { Path::new(".zed_server") } +/// Returns the relative path to the zed_wsl_server directory on the wsl host. +pub fn remote_wsl_server_dir_relative() -> &'static Path { + Path::new(".zed_wsl_server") +} + /// Sets a custom directory for all user data, overriding the default data directory. /// This function must be called before any other path operations that depend on the data directory. /// The directory's path will be canonicalized to an absolute path by a blocking FS operation. diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe..6c1449b728d3ee5b8c8b019d5e527e9adfb3bf25 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -258,8 +258,14 @@ impl DapStore { let connection; if let Some(c) = binary.connection { let host = Ipv4Addr::LOCALHOST; - let port = dap::transport::TcpTransport::unused_port(host).await?; - port_forwarding = Some((port, c.host.to_string(), c.port)); + let port; + if remote.read_with(cx, |remote, _cx| remote.shares_network_interface())? { + port = c.port; + port_forwarding = None; + } else { + port = dap::transport::TcpTransport::unused_port(host).await?; + port_forwarding = Some((port, c.host.to_string(), c.port)); + } connection = Some(TcpArguments { port, host, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b32e95741f522650e5d20f80a6ba18c423805234..557367edf522a103ee1a8b55f5264be561d1698e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -87,7 +87,7 @@ use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; -use remote::{RemoteClient, SshConnectionOptions}; +use remote::{RemoteClient, RemoteConnectionOptions}; use rpc::{ AnyProtoClient, ErrorCode, proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto}, @@ -1916,7 +1916,7 @@ impl Project { .map(|remote| remote.read(cx).connection_state()) } - pub fn remote_connection_options(&self, cx: &App) -> Option { + pub fn remote_connection_options(&self, cx: &App) -> Option { self.remote_client .as_ref() .map(|remote| remote.read(cx).connection_options()) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index c189242fadc2948593186edb5dcd2c56879f07af..597da04617e9670e623196ef21f02c366e49d392 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -512,7 +512,7 @@ fn create_remote_shell( *env = command.env; log::debug!("Connecting to a remote server: {:?}", command.program); - let host = remote_client.read(cx).connection_options().host; + let host = remote_client.read(cx).connection_options().display_name(); Ok(Shell::WithArguments { program: command.program, diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 36da6897b92e4bc183aa7c0f51d5100e8836931e..c97f7062a8206052e7c63f6bec909dd5823dbedf 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,6 +1,6 @@ use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; -use remote::SshConnectionOptions; +use remote::RemoteConnectionOptions; use settings::Settings; use ui::{ Button, ButtonCommon, ButtonStyle, Clickable, Context, ElevationIndex, FluentBuilder, Headline, @@ -9,11 +9,11 @@ use ui::{ }; use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; -use crate::open_ssh_project; +use crate::open_remote_project; enum Host { - RemoteProject, - SshRemoteProject(SshConnectionOptions), + CollabGuestProject, + RemoteServerProject(RemoteConnectionOptions), } pub struct DisconnectedOverlay { @@ -66,9 +66,9 @@ impl DisconnectedOverlay { let remote_connection_options = project.read(cx).remote_connection_options(cx); let host = if let Some(ssh_connection_options) = remote_connection_options { - Host::SshRemoteProject(ssh_connection_options) + Host::RemoteServerProject(ssh_connection_options) } else { - Host::RemoteProject + Host::CollabGuestProject }; workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay { @@ -86,14 +86,14 @@ impl DisconnectedOverlay { self.finished = true; cx.emit(DismissEvent); - if let Host::SshRemoteProject(ssh_connection_options) = &self.host { - self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx); + if let Host::RemoteServerProject(ssh_connection_options) = &self.host { + self.reconnect_to_remote_project(ssh_connection_options.clone(), window, cx); } } - fn reconnect_to_ssh_remote( + fn reconnect_to_remote_project( &self, - connection_options: SshConnectionOptions, + connection_options: RemoteConnectionOptions, window: &mut Window, cx: &mut Context, ) { @@ -114,7 +114,7 @@ impl DisconnectedOverlay { .collect(); cx.spawn_in(window, async move |_, cx| { - open_ssh_project( + open_remote_project( connection_options, paths, app_state, @@ -138,13 +138,13 @@ impl DisconnectedOverlay { impl Render for DisconnectedOverlay { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let can_reconnect = matches!(self.host, Host::SshRemoteProject(_)); + let can_reconnect = matches!(self.host, Host::RemoteServerProject(_)); let message = match &self.host { - Host::RemoteProject => { + Host::CollabGuestProject => { "Your connection to the remote project has been lost.".to_string() } - Host::SshRemoteProject(options) => { + Host::RemoteServerProject(options) => { let autosave = if ProjectSettings::get_global(cx) .session .restore_unsaved_buffers @@ -155,7 +155,8 @@ impl Render for DisconnectedOverlay { }; format!( "Your connection to {} has been lost.{}", - options.host, autosave + options.display_name(), + autosave ) } }; diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index fa57b588cd8457788adc0226264a4871c3305b85..aa0ce7661b29123c25fdf20cbde5f53e6525d2d6 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,9 +1,10 @@ pub mod disconnected_overlay; +mod remote_connections; mod remote_servers; mod ssh_config; -mod ssh_connections; -pub use ssh_connections::{is_connecting_over_ssh, open_ssh_project}; +use remote::RemoteConnectionOptions; +pub use remote_connections::open_remote_project; use disconnected_overlay::DisconnectedOverlay; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -16,9 +17,9 @@ use picker::{ Picker, PickerDelegate, highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths}, }; +pub use remote_connections::SshSettings; pub use remote_servers::RemoteServerProjects; use settings::Settings; -pub use ssh_connections::SshSettings; use std::{path::Path, sync::Arc}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; @@ -290,7 +291,7 @@ impl PickerDelegate for RecentProjectsDelegate { if workspace.database_id() == Some(*candidate_workspace_id) { Task::ready(Ok(())) } else { - match candidate_workspace_location { + match candidate_workspace_location.clone() { SerializedWorkspaceLocation::Local => { let paths = candidate_workspace_paths.paths().to_vec(); if replace_current_window { @@ -320,7 +321,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(connection) => { + SerializedWorkspaceLocation::Remote(mut connection) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -334,18 +335,16 @@ impl PickerDelegate for RecentProjectsDelegate { ..Default::default() }; - let connection_options = SshSettings::get_global(cx) - .connection_options_for( - connection.host.clone(), - connection.port, - connection.user.clone(), - ); + if let RemoteConnectionOptions::Ssh(connection) = &mut connection { + SshSettings::get_global(cx) + .fill_connection_options_from_settings(connection); + }; let paths = candidate_workspace_paths.paths().to_vec(); cx.spawn_in(window, async move |_, cx| { - open_ssh_project( - connection_options, + open_remote_project( + connection.clone(), paths, app_state, open_options, @@ -418,9 +417,11 @@ impl PickerDelegate for RecentProjectsDelegate { SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) .color(Color::Muted) .into_any_element(), - SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) - .color(Color::Muted) - .into_any_element(), + SerializedWorkspaceLocation::Remote(_) => { + Icon::new(IconName::Server) + .color(Color::Muted) + .into_any_element() + } }) }) .child({ diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/remote_connections.rs similarity index 85% rename from crates/recent_projects/src/ssh_connections.rs rename to crates/recent_projects/src/remote_connections.rs index 29f6e75bbdebf72b36295b20295f0705b636214e..47607813b547e28b9b4a37449f8daaa6ec022764 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -16,7 +16,8 @@ use language::CursorShape; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; use remote::{ - ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption, + ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform, + SshConnectionOptions, SshPortForwardOption, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -42,32 +43,35 @@ impl SshSettings { self.ssh_connections.clone().into_iter().flatten() } + pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) { + for conn in self.ssh_connections() { + if conn.host == options.host + && conn.username == options.username + && conn.port == options.port + { + options.nickname = conn.nickname; + options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default(); + options.args = Some(conn.args); + options.port_forwards = conn.port_forwards; + break; + } + } + } + pub fn connection_options_for( &self, host: String, port: Option, username: Option, ) -> SshConnectionOptions { - for conn in self.ssh_connections() { - if conn.host == host && conn.username == username && conn.port == port { - return SshConnectionOptions { - nickname: conn.nickname, - upload_binary_over_ssh: conn.upload_binary_over_ssh.unwrap_or_default(), - args: Some(conn.args), - host, - port, - username, - port_forwards: conn.port_forwards, - password: None, - }; - } - } - SshConnectionOptions { + let mut options = SshConnectionOptions { host, port, username, ..Default::default() - } + }; + self.fill_connection_options_from_settings(&mut options); + options } } @@ -135,7 +139,7 @@ impl Settings for SshSettings { fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } -pub struct SshPrompt { +pub struct RemoteConnectionPrompt { connection_string: SharedString, nickname: Option, status_message: Option, @@ -144,7 +148,7 @@ pub struct SshPrompt { editor: Entity, } -impl Drop for SshPrompt { +impl Drop for RemoteConnectionPrompt { fn drop(&mut self) { if let Some(cancel) = self.cancellation.take() { cancel.send(()).ok(); @@ -152,24 +156,22 @@ impl Drop for SshPrompt { } } -pub struct SshConnectionModal { - pub(crate) prompt: Entity, +pub struct RemoteConnectionModal { + pub(crate) prompt: Entity, paths: Vec, finished: bool, } -impl SshPrompt { +impl RemoteConnectionPrompt { pub(crate) fn new( - connection_options: &SshConnectionOptions, + connection_string: String, + nickname: Option, window: &mut Window, cx: &mut Context, ) -> Self { - let connection_string = connection_options.connection_string().into(); - let nickname = connection_options.nickname.clone().map(|s| s.into()); - Self { - connection_string, - nickname, + connection_string: connection_string.into(), + nickname: nickname.map(|nickname| nickname.into()), editor: cx.new(|cx| Editor::single_line(window, cx)), status_message: None, cancellation: None, @@ -232,7 +234,7 @@ impl SshPrompt { } } -impl Render for SshPrompt { +impl Render for RemoteConnectionPrompt { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = ThemeSettings::get_global(cx); @@ -297,15 +299,22 @@ impl Render for SshPrompt { } } -impl SshConnectionModal { +impl RemoteConnectionModal { pub(crate) fn new( - connection_options: &SshConnectionOptions, + connection_options: &RemoteConnectionOptions, paths: Vec, window: &mut Window, cx: &mut Context, ) -> Self { + let (connection_string, nickname) = match connection_options { + RemoteConnectionOptions::Ssh(options) => { + (options.connection_string(), options.nickname.clone()) + } + RemoteConnectionOptions::Wsl(options) => (options.distro_name.clone(), None), + }; Self { - prompt: cx.new(|cx| SshPrompt::new(connection_options, window, cx)), + prompt: cx + .new(|cx| RemoteConnectionPrompt::new(connection_string, nickname, window, cx)), finished: false, paths, } @@ -386,7 +395,7 @@ impl RenderOnce for SshConnectionHeader { } } -impl Render for SshConnectionModal { +impl Render for RemoteConnectionModal { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let nickname = self.prompt.read(cx).nickname.clone(); let connection_string = self.prompt.read(cx).connection_string.clone(); @@ -423,15 +432,15 @@ impl Render for SshConnectionModal { } } -impl Focusable for SshConnectionModal { +impl Focusable for RemoteConnectionModal { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.prompt.read(cx).editor.focus_handle(cx) } } -impl EventEmitter for SshConnectionModal {} +impl EventEmitter for RemoteConnectionModal {} -impl ModalView for SshConnectionModal { +impl ModalView for RemoteConnectionModal { fn on_before_dismiss( &mut self, _window: &mut Window, @@ -446,13 +455,13 @@ impl ModalView for SshConnectionModal { } #[derive(Clone)] -pub struct SshClientDelegate { +pub struct RemoteClientDelegate { window: AnyWindowHandle, - ui: WeakEntity, + ui: WeakEntity, known_password: Option, } -impl remote::RemoteClientDelegate for SshClientDelegate { +impl remote::RemoteClientDelegate for RemoteClientDelegate { fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp) { let mut known_password = self.known_password.clone(); if let Some(password) = known_password.take() { @@ -522,7 +531,7 @@ impl remote::RemoteClientDelegate for SshClientDelegate { } } -impl SshClientDelegate { +impl RemoteClientDelegate { fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) { self.window .update(cx, |_, _, cx| { @@ -534,14 +543,10 @@ impl SshClientDelegate { } } -pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &App) -> bool { - workspace.active_modal::(cx).is_some() -} - pub fn connect_over_ssh( unique_identifier: ConnectionIdentifier, connection_options: SshConnectionOptions, - ui: Entity, + ui: Entity, window: &mut Window, cx: &mut App, ) -> Task>>> { @@ -554,7 +559,7 @@ pub fn connect_over_ssh( unique_identifier, connection_options, rx, - Arc::new(SshClientDelegate { + Arc::new(RemoteClientDelegate { window, ui: ui.downgrade(), known_password, @@ -563,8 +568,8 @@ pub fn connect_over_ssh( ) } -pub async fn open_ssh_project( - connection_options: SshConnectionOptions, +pub async fn open_remote_project( + connection_options: RemoteConnectionOptions, paths: Vec, app_state: Arc, open_options: workspace::OpenOptions, @@ -575,13 +580,7 @@ pub async fn open_ssh_project( } else { let workspace_position = cx .update(|cx| { - workspace::ssh_workspace_position_from_db( - connection_options.host.clone(), - connection_options.port, - connection_options.username.clone(), - &paths, - cx, - ) + workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) })? .await .context("fetching ssh workspace position from db")?; @@ -611,16 +610,16 @@ pub async fn open_ssh_project( loop { let (cancel_tx, cancel_rx) = oneshot::channel(); let delegate = window.update(cx, { - let connection_options = connection_options.clone(); let paths = paths.clone(); + let connection_options = connection_options.clone(); move |workspace, window, cx| { window.activate_window(); workspace.toggle_modal(window, cx, |window, cx| { - SshConnectionModal::new(&connection_options, paths, window, cx) + RemoteConnectionModal::new(&connection_options, paths, window, cx) }); let ui = workspace - .active_modal::(cx)? + .active_modal::(cx)? .read(cx) .prompt .clone(); @@ -629,19 +628,25 @@ pub async fn open_ssh_project( ui.set_cancellation_tx(cancel_tx); }); - Some(Arc::new(SshClientDelegate { + Some(Arc::new(RemoteClientDelegate { window: window.window_handle(), ui: ui.downgrade(), - known_password: connection_options.password.clone(), + known_password: if let RemoteConnectionOptions::Ssh(options) = + &connection_options + { + options.password.clone() + } else { + None + }, })) } })?; let Some(delegate) = delegate else { break }; - let did_open_ssh_project = cx + let did_open_project = cx .update(|cx| { - workspace::open_ssh_project_with_new_connection( + workspace::open_remote_project_with_new_connection( window, connection_options.clone(), cancel_rx, @@ -655,19 +660,22 @@ pub async fn open_ssh_project( window .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { + if let Some(ui) = workspace.active_modal::(cx) { ui.update(cx, |modal, cx| modal.finished(cx)) } }) .ok(); - if let Err(e) = did_open_ssh_project { + if let Err(e) = did_open_project { log::error!("Failed to open project: {e:?}"); let response = window .update(cx, |_, window, cx| { window.prompt( PromptLevel::Critical, - "Failed to connect over SSH", + match connection_options { + RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", + RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", + }, Some(&e.to_string()), &["Retry", "Ok"], cx, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index f4fd1f1c1bbb12e2fbf11088baf859b08bfbf310..3cf084bef76a56cf85973f67bb5713aee59fb1bc 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,70 +1,52 @@ -use std::any::Any; -use std::borrow::Cow; -use std::collections::BTreeSet; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic; -use std::sync::atomic::AtomicUsize; - +use crate::{ + remote_connections::{ + RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection, + SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project, + }, + ssh_config::parse_ssh_config_hosts, +}; use editor::Editor; use file_finder::OpenPathDelegate; -use futures::FutureExt; -use futures::channel::oneshot; -use futures::future::Shared; -use futures::select; -use gpui::ClickEvent; -use gpui::ClipboardItem; -use gpui::Subscription; -use gpui::Task; -use gpui::WeakEntity; -use gpui::canvas; +use futures::{FutureExt, channel::oneshot, future::Shared, select}; use gpui::{ - AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - PromptLevel, ScrollHandle, Window, + AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, + canvas, }; -use paths::global_ssh_config_file; -use paths::user_ssh_config_file; +use paths::{global_ssh_config_file, user_ssh_config_file}; use picker::Picker; -use project::Fs; -use project::Project; -use remote::remote_client::ConnectionIdentifier; -use remote::{RemoteClient, SshConnectionOptions}; -use settings::Settings; -use settings::SettingsStore; -use settings::update_settings_file; -use settings::watch_config_file; +use project::{Fs, Project}; +use remote::{ + RemoteClient, RemoteConnectionOptions, SshConnectionOptions, + remote_client::ConnectionIdentifier, +}; +use settings::{Settings, SettingsStore, update_settings_file, watch_config_file}; use smol::stream::StreamExt as _; -use ui::Navigable; -use ui::NavigableEntry; +use std::{ + any::Any, + borrow::Cow, + collections::BTreeSet, + path::PathBuf, + rc::Rc, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; use ui::{ - IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, - Section, Tooltip, prelude::*, + IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Navigable, NavigableEntry, + Scrollbar, ScrollbarState, Section, Tooltip, prelude::*, }; use util::{ ResultExt, paths::{PathStyle, RemotePathBuf}, }; -use workspace::OpenOptions; -use workspace::Toast; -use workspace::notifications::NotificationId; use workspace::{ - ModalView, Workspace, notifications::DetachAndPromptErr, - open_ssh_project_with_existing_connection, + ModalView, OpenOptions, Toast, Workspace, + notifications::{DetachAndPromptErr, NotificationId}, + open_remote_project_with_existing_connection, }; -use crate::ssh_config::parse_ssh_config_hosts; -use crate::ssh_connections::RemoteSettingsContent; -use crate::ssh_connections::SshConnection; -use crate::ssh_connections::SshConnectionHeader; -use crate::ssh_connections::SshConnectionModal; -use crate::ssh_connections::SshProject; -use crate::ssh_connections::SshPrompt; -use crate::ssh_connections::SshSettings; -use crate::ssh_connections::connect_over_ssh; -use crate::ssh_connections::open_ssh_project; - -mod navigation_base {} pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, @@ -79,7 +61,7 @@ pub struct RemoteServerProjects { struct CreateRemoteServer { address_editor: Entity, address_error: Option, - ssh_prompt: Option>, + ssh_prompt: Option>, _creating: Option>>, } @@ -222,8 +204,13 @@ impl ProjectPicker { }) .log_err()?; - open_ssh_project_with_existing_connection( - connection, project, paths, app_state, window, cx, + open_remote_project_with_existing_connection( + RemoteConnectionOptions::Ssh(connection), + project, + paths, + app_state, + window, + cx, ) .await .log_err(); @@ -472,7 +459,14 @@ impl RemoteServerProjects { return; } }; - let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx)); + let ssh_prompt = cx.new(|cx| { + RemoteConnectionPrompt::new( + connection_options.connection_string(), + connection_options.nickname.clone(), + window, + cx, + ) + }); let connection = connect_over_ssh( ConnectionIdentifier::setup(), @@ -552,15 +546,20 @@ impl RemoteServerProjects { }; let create_new_window = self.create_new_window; - let connection_options = ssh_connection.into(); + let connection_options: SshConnectionOptions = ssh_connection.into(); workspace.update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { let app_state = workspace.app_state().clone(); workspace.toggle_modal(window, cx, |window, cx| { - SshConnectionModal::new(&connection_options, Vec::new(), window, cx) + RemoteConnectionModal::new( + &RemoteConnectionOptions::Ssh(connection_options.clone()), + Vec::new(), + window, + cx, + ) }); let prompt = workspace - .active_modal::(cx) + .active_modal::(cx) .unwrap() .read(cx) .prompt @@ -579,7 +578,7 @@ impl RemoteServerProjects { let session = connect.await; workspace.update(cx, |workspace, cx| { - if let Some(prompt) = workspace.active_modal::(cx) { + if let Some(prompt) = workspace.active_modal::(cx) { prompt.update(cx, |prompt, cx| prompt.finished(cx)) } })?; @@ -898,8 +897,8 @@ impl RemoteServerProjects { }; cx.spawn_in(window, async move |_, cx| { - let result = open_ssh_project( - server.into(), + let result = open_remote_project( + RemoteConnectionOptions::Ssh(server.into()), project.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions { diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index c698353d9edfc0d48c7039f321a2c88890e8c098..74d45b1a696ff1a02a9f2b4d9afc3844f82196cd 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -6,6 +6,7 @@ mod transport; pub use remote_client::{ ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, - RemotePlatform, + RemoteConnectionOptions, RemotePlatform, }; pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; +pub use transport::wsl::WslConnectionOptions; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 7e231e622cb2336a113799f7087fc0e30a5f79ff..501c6a8dd639630b1930cb32e804f8cca658a9ca 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -1,6 +1,11 @@ use crate::{ - SshConnectionOptions, protocol::MessageId, proxy::ProxyLaunchError, - transport::ssh::SshRemoteConnection, + SshConnectionOptions, + protocol::MessageId, + proxy::ProxyLaunchError, + transport::{ + ssh::SshRemoteConnection, + wsl::{WslConnectionOptions, WslRemoteConnection}, + }, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; @@ -237,7 +242,7 @@ impl From<&State> for ConnectionState { pub struct RemoteClient { client: Arc, unique_identifier: String, - connection_options: SshConnectionOptions, + connection_options: RemoteConnectionOptions, path_style: PathStyle, state: Option, } @@ -290,6 +295,22 @@ impl RemoteClient { cancellation: oneshot::Receiver<()>, delegate: Arc, cx: &mut App, + ) -> Task>>> { + Self::new( + unique_identifier, + RemoteConnectionOptions::Ssh(connection_options), + cancellation, + delegate, + cx, + ) + } + + pub fn new( + unique_identifier: ConnectionIdentifier, + connection_options: RemoteConnectionOptions, + cancellation: oneshot::Receiver<()>, + delegate: Arc, + cx: &mut App, ) -> Task>>> { let unique_identifier = unique_identifier.to_string(cx); cx.spawn(async move |cx| { @@ -424,7 +445,7 @@ impl RemoteClient { } let state = self.state.take().unwrap(); - let (attempts, ssh_connection, delegate) = match state { + let (attempts, remote_connection, delegate) = match state { State::Connected { ssh_connection, delegate, @@ -482,15 +503,15 @@ impl RemoteClient { }; } - if let Err(error) = ssh_connection + if let Err(error) = remote_connection .kill() .await .context("Failed to kill ssh process") { - failed!(error, attempts, ssh_connection, delegate); + failed!(error, attempts, remote_connection, delegate); }; - let connection_options = ssh_connection.connection_options(); + let connection_options = remote_connection.connection_options(); let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); let (incoming_tx, incoming_rx) = mpsc::unbounded::(); @@ -519,7 +540,7 @@ impl RemoteClient { { Ok((ssh_connection, io_task)) => (ssh_connection, io_task), Err(error) => { - failed!(error, attempts, ssh_connection, delegate); + failed!(error, attempts, remote_connection, delegate); } }; @@ -751,6 +772,13 @@ impl RemoteClient { Some(self.state.as_ref()?.remote_connection()?.shell()) } + pub fn shares_network_interface(&self) -> bool { + self.state + .as_ref() + .and_then(|state| state.remote_connection()) + .map_or(false, |connection| connection.shares_network_interface()) + } + pub fn build_command( &self, program: Option, @@ -789,11 +817,7 @@ impl RemoteClient { self.client.clone().into() } - pub fn host(&self) -> String { - self.connection_options.host.clone() - } - - pub fn connection_options(&self) -> SshConnectionOptions { + pub fn connection_options(&self) -> RemoteConnectionOptions { self.connection_options.clone() } @@ -836,14 +860,14 @@ impl RemoteClient { pub fn fake_server( client_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext, - ) -> (SshConnectionOptions, AnyProtoClient) { + ) -> (RemoteConnectionOptions, AnyProtoClient) { let port = client_cx .update(|cx| cx.default_global::().connections.len() as u16 + 1); - let opts = SshConnectionOptions { + let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "".to_string(), port: Some(port), ..Default::default() - }; + }); let (outgoing_tx, _) = mpsc::unbounded::(); let (_, incoming_rx) = mpsc::unbounded::(); let server_client = @@ -874,13 +898,13 @@ impl RemoteClient { #[cfg(any(test, feature = "test-support"))] pub async fn fake_client( - opts: SshConnectionOptions, + opts: RemoteConnectionOptions, client_cx: &mut gpui::TestAppContext, ) -> Entity { let (_tx, rx) = oneshot::channel(); client_cx .update(|cx| { - Self::ssh( + Self::new( ConnectionIdentifier::setup(), opts, rx, @@ -901,7 +925,7 @@ enum ConnectionPoolEntry { #[derive(Default)] struct ConnectionPool { - connections: HashMap, + connections: HashMap, } impl Global for ConnectionPool {} @@ -909,7 +933,7 @@ impl Global for ConnectionPool {} impl ConnectionPool { pub fn connect( &mut self, - opts: SshConnectionOptions, + opts: RemoteConnectionOptions, delegate: &Arc, cx: &mut App, ) -> Shared, Arc>>> { @@ -939,9 +963,18 @@ impl ConnectionPool { let opts = opts.clone(); let delegate = delegate.clone(); async move |cx| { - let connection = SshRemoteConnection::new(opts.clone(), delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc); + let connection = match opts.clone() { + RemoteConnectionOptions::Ssh(opts) => { + SshRemoteConnection::new(opts, delegate, cx) + .await + .map(|connection| Arc::new(connection) as Arc) + } + RemoteConnectionOptions::Wsl(opts) => { + WslRemoteConnection::new(opts, delegate, cx) + .await + .map(|connection| Arc::new(connection) as Arc) + } + }; cx.update_global(|pool: &mut Self, _| { debug_assert!(matches!( @@ -972,6 +1005,33 @@ impl ConnectionPool { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum RemoteConnectionOptions { + Ssh(SshConnectionOptions), + Wsl(WslConnectionOptions), +} + +impl RemoteConnectionOptions { + pub fn display_name(&self) -> String { + match self { + RemoteConnectionOptions::Ssh(opts) => opts.host.clone(), + RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(), + } + } +} + +impl From for RemoteConnectionOptions { + fn from(opts: SshConnectionOptions) -> Self { + RemoteConnectionOptions::Ssh(opts) + } +} + +impl From for RemoteConnectionOptions { + fn from(opts: WslConnectionOptions) -> Self { + RemoteConnectionOptions::Wsl(opts) + } +} + #[async_trait(?Send)] pub(crate) trait RemoteConnection: Send + Sync { fn start_proxy( @@ -992,6 +1052,9 @@ pub(crate) trait RemoteConnection: Send + Sync { ) -> Task>; async fn kill(&self) -> Result<()>; fn has_been_killed(&self) -> bool; + fn shares_network_interface(&self) -> bool { + false + } fn build_command( &self, program: Option, @@ -1000,7 +1063,7 @@ pub(crate) trait RemoteConnection: Send + Sync { working_dir: Option, port_forward: Option<(u16, String, u16)>, ) -> Result; - fn connection_options(&self) -> SshConnectionOptions; + fn connection_options(&self) -> RemoteConnectionOptions; fn path_style(&self) -> PathStyle; fn shell(&self) -> String; @@ -1307,7 +1370,7 @@ impl ProtoClient for ChannelClient { #[cfg(any(test, feature = "test-support"))] mod fake { use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform}; - use crate::{SshConnectionOptions, remote_client::CommandTemplate}; + use crate::remote_client::{CommandTemplate, RemoteConnectionOptions}; use anyhow::Result; use async_trait::async_trait; use collections::HashMap; @@ -1326,7 +1389,7 @@ mod fake { use util::paths::{PathStyle, RemotePathBuf}; pub(super) struct FakeRemoteConnection { - pub(super) connection_options: SshConnectionOptions, + pub(super) connection_options: RemoteConnectionOptions, pub(super) server_channel: Arc, pub(super) server_cx: SendableCx, } @@ -1386,7 +1449,7 @@ mod fake { unreachable!() } - fn connection_options(&self) -> SshConnectionOptions { + fn connection_options(&self) -> RemoteConnectionOptions { self.connection_options.clone() } diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs index aa086fd3f56196e71224ef346c9810e8638c5c47..36525b7fcc1d91f106cffb6592a1ffd8e5e96fa9 100644 --- a/crates/remote/src/transport.rs +++ b/crates/remote/src/transport.rs @@ -1 +1,336 @@ +use crate::{ + json_log::LogRecord, + protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, +}; +use anyhow::{Context as _, Result}; +use futures::{ + AsyncReadExt as _, FutureExt as _, StreamExt as _, + channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, +}; +use gpui::{AppContext as _, AsyncApp, Task}; +use rpc::proto::Envelope; +use smol::process::Child; + pub mod ssh; +pub mod wsl; + +fn handle_rpc_messages_over_child_process_stdio( + mut ssh_proxy_process: Child, + incoming_tx: UnboundedSender, + mut outgoing_rx: UnboundedReceiver, + mut connection_activity_tx: Sender<()>, + cx: &AsyncApp, +) -> Task> { + let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); + let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); + let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); + + let mut stdin_buffer = Vec::new(); + let mut stdout_buffer = Vec::new(); + let mut stderr_buffer = Vec::new(); + let mut stderr_offset = 0; + + let stdin_task = cx.background_spawn(async move { + while let Some(outgoing) = outgoing_rx.next().await { + write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; + } + anyhow::Ok(()) + }); + + let stdout_task = cx.background_spawn({ + let mut connection_activity_tx = connection_activity_tx.clone(); + async move { + loop { + stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); + let len = child_stdout.read(&mut stdout_buffer).await?; + + if len == 0 { + return anyhow::Ok(()); + } + + if len < MESSAGE_LEN_SIZE { + child_stdout.read_exact(&mut stdout_buffer[len..]).await?; + } + + let message_len = message_len_from_buffer(&stdout_buffer); + let envelope = + read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) + .await?; + connection_activity_tx.try_send(()).ok(); + incoming_tx.unbounded_send(envelope).ok(); + } + } + }); + + let stderr_task: Task> = cx.background_spawn(async move { + loop { + stderr_buffer.resize(stderr_offset + 1024, 0); + + let len = child_stderr + .read(&mut stderr_buffer[stderr_offset..]) + .await?; + if len == 0 { + return anyhow::Ok(()); + } + + stderr_offset += len; + let mut start_ix = 0; + while let Some(ix) = stderr_buffer[start_ix..stderr_offset] + .iter() + .position(|b| b == &b'\n') + { + let line_ix = start_ix + ix; + let content = &stderr_buffer[start_ix..line_ix]; + start_ix = line_ix + 1; + if let Ok(record) = serde_json::from_slice::(content) { + record.log(log::logger()) + } else { + eprintln!("(remote) {}", String::from_utf8_lossy(content)); + } + } + stderr_buffer.drain(0..start_ix); + stderr_offset -= start_ix; + + connection_activity_tx.try_send(()).ok(); + } + }); + + cx.background_spawn(async move { + let result = futures::select! { + result = stdin_task.fuse() => { + result.context("stdin") + } + result = stdout_task.fuse() => { + result.context("stdout") + } + result = stderr_task.fuse() => { + result.context("stderr") + } + }; + + let status = ssh_proxy_process.status().await?.code().unwrap_or(1); + match result { + Ok(_) => Ok(status), + Err(error) => Err(error), + } + }) +} + +#[cfg(debug_assertions)] +async fn build_remote_server_from_source( + platform: &crate::RemotePlatform, + delegate: &dyn crate::RemoteClientDelegate, + cx: &mut AsyncApp, +) -> Result> { + use std::path::Path; + + let Some(build_remote_server) = std::env::var("ZED_BUILD_REMOTE_SERVER").ok() else { + return Ok(None); + }; + + use smol::process::{Command, Stdio}; + use std::env::VarError; + + async fn run_cmd(command: &mut Command) -> Result<()> { + let output = command + .kill_on_drop(true) + .stderr(Stdio::inherit()) + .output() + .await?; + anyhow::ensure!( + output.status.success(), + "Failed to run command: {command:?}" + ); + Ok(()) + } + + let use_musl = !build_remote_server.contains("nomusl"); + let triple = format!( + "{}-{}", + platform.arch, + match platform.os { + "linux" => + if use_musl { + "unknown-linux-musl" + } else { + "unknown-linux-gnu" + }, + "macos" => "apple-darwin", + _ => anyhow::bail!("can't cross compile for: {:?}", platform), + } + ); + let mut rust_flags = match std::env::var("RUSTFLAGS") { + Ok(val) => val, + Err(VarError::NotPresent) => String::new(), + Err(e) => { + log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); + String::new() + } + }; + if platform.os == "linux" && use_musl { + rust_flags.push_str(" -C target-feature=+crt-static"); + } + if build_remote_server.contains("mold") { + rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); + } + + 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", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + 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); + + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; + + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + format!("--mount type=bind,src={src},dst=/app/target"), + ) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else { + let which = cx + .background_spawn(async move { which::which("zig") }) + .await; + + if which.is_err() { + #[cfg(not(target_os = "windows"))] + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + } + + delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); + log::info!("adding rustup target"); + run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; + + delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); + log::info!("installing cargo-zigbuild"); + run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; + + delegate.set_status( + Some(&format!( + "Building remote binary from source for {triple} with Zig" + )), + cx, + ); + log::info!("building remote binary from source for {triple} with Zig"); + run_cmd( + Command::new("cargo") + .args([ + "zigbuild", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + }; + let bin_path = Path::new("target") + .join("remote_server") + .join(&triple) + .join("debug") + .join("remote_server"); + + let path = if !build_remote_server.contains("nocompress") { + delegate.set_status(Some("Compressing binary"), cx); + + #[cfg(not(target_os = "windows"))] + { + run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; + } + + #[cfg(target_os = "windows")] + { + // On Windows, we use 7z to compress the binary + let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; + let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); + if smol::fs::metadata(&gz_path).await.is_ok() { + smol::fs::remove_file(&gz_path).await?; + } + run_cmd(Command::new(seven_zip).args([ + "a", + "-tgzip", + &gz_path, + &bin_path.to_string_lossy(), + ])) + .await?; + } + + let mut archive_path = bin_path; + archive_path.set_extension("gz"); + std::env::current_dir()?.join(archive_path) + } else { + bin_path + }; + + Ok(Some(path)) +} diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 34f1ebf71c278538b57e486856f9b3315a41cf91..0995e0dd611ae667cc2e68638773c8b80bf2f22b 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -1,14 +1,12 @@ use crate::{ RemoteClientDelegate, RemotePlatform, - json_log::LogRecord, - protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, - remote_client::{CommandTemplate, RemoteConnection}, + remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; use collections::HashMap; use futures::{ - AsyncReadExt as _, FutureExt as _, StreamExt as _, + AsyncReadExt as _, FutureExt as _, channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, select_biased, }; @@ -99,8 +97,8 @@ impl RemoteConnection for SshRemoteConnection { self.master_process.lock().is_none() } - fn connection_options(&self) -> SshConnectionOptions { - self.socket.connection_options.clone() + fn connection_options(&self) -> RemoteConnectionOptions { + RemoteConnectionOptions::Ssh(self.socket.connection_options.clone()) } fn shell(&self) -> String { @@ -267,7 +265,7 @@ impl RemoteConnection for SshRemoteConnection { } }; - Self::multiplex( + super::handle_rpc_messages_over_child_process_stdio( ssh_proxy_process, incoming_tx, outgoing_rx, @@ -415,109 +413,6 @@ impl SshRemoteConnection { Ok(this) } - fn multiplex( - mut ssh_proxy_process: Child, - incoming_tx: UnboundedSender, - mut outgoing_rx: UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - cx: &AsyncApp, - ) -> Task> { - let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); - let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); - let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); - - let mut stdin_buffer = Vec::new(); - let mut stdout_buffer = Vec::new(); - let mut stderr_buffer = Vec::new(); - let mut stderr_offset = 0; - - let stdin_task = cx.background_spawn(async move { - while let Some(outgoing) = outgoing_rx.next().await { - write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; - } - anyhow::Ok(()) - }); - - let stdout_task = cx.background_spawn({ - let mut connection_activity_tx = connection_activity_tx.clone(); - async move { - loop { - stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); - let len = child_stdout.read(&mut stdout_buffer).await?; - - if len == 0 { - return anyhow::Ok(()); - } - - if len < MESSAGE_LEN_SIZE { - child_stdout.read_exact(&mut stdout_buffer[len..]).await?; - } - - let message_len = message_len_from_buffer(&stdout_buffer); - let envelope = - read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) - .await?; - connection_activity_tx.try_send(()).ok(); - incoming_tx.unbounded_send(envelope).ok(); - } - } - }); - - let stderr_task: Task> = cx.background_spawn(async move { - loop { - stderr_buffer.resize(stderr_offset + 1024, 0); - - let len = child_stderr - .read(&mut stderr_buffer[stderr_offset..]) - .await?; - if len == 0 { - return anyhow::Ok(()); - } - - stderr_offset += len; - let mut start_ix = 0; - while let Some(ix) = stderr_buffer[start_ix..stderr_offset] - .iter() - .position(|b| b == &b'\n') - { - let line_ix = start_ix + ix; - let content = &stderr_buffer[start_ix..line_ix]; - start_ix = line_ix + 1; - if let Ok(record) = serde_json::from_slice::(content) { - record.log(log::logger()) - } else { - eprintln!("(remote) {}", String::from_utf8_lossy(content)); - } - } - stderr_buffer.drain(0..start_ix); - stderr_offset -= start_ix; - - connection_activity_tx.try_send(()).ok(); - } - }); - - cx.background_spawn(async move { - let result = futures::select! { - result = stdin_task.fuse() => { - result.context("stdin") - } - result = stdout_task.fuse() => { - result.context("stdout") - } - result = stderr_task.fuse() => { - result.context("stderr") - } - }; - - let status = ssh_proxy_process.status().await?.code().unwrap_or(1); - match result { - Ok(_) => Ok(status), - Err(error) => Err(error), - } - }) - } - - #[allow(unused)] async fn ensure_server_binary( &self, delegate: &Arc, @@ -544,19 +439,20 @@ impl SshRemoteConnection { self.ssh_path_style, ); - let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok(); #[cfg(debug_assertions)] - if let Some(build_remote_server) = build_remote_server { - let src_path = self.build_local(build_remote_server, delegate, cx).await?; + if let Some(remote_server_path) = + super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx) + .await? + { let tmp_path = RemotePathBuf::new( paths::remote_server_dir_relative().join(format!( "download-{}-{}", std::process::id(), - src_path.file_name().unwrap().to_string_lossy() + remote_server_path.file_name().unwrap().to_string_lossy() )), self.ssh_path_style, ); - self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) + self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx) .await?; self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) .await?; @@ -794,221 +690,6 @@ impl SshRemoteConnection { ); Ok(()) } - - #[cfg(debug_assertions)] - async fn build_local( - &self, - build_remote_server: String, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result { - use smol::process::{Command, Stdio}; - use std::env::VarError; - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - anyhow::ensure!( - output.status.success(), - "Failed to run command: {command:?}" - ); - Ok(()) - } - - let use_musl = !build_remote_server.contains("nomusl"); - let triple = format!( - "{}-{}", - self.ssh_platform.arch, - match self.ssh_platform.os { - "linux" => - if use_musl { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), - } - ); - let mut rust_flags = match std::env::var("RUSTFLAGS") { - Ok(val) => val, - Err(VarError::NotPresent) => String::new(), - Err(e) => { - log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); - String::new() - } - }; - if self.ssh_platform.os == "linux" && use_musl { - rust_flags.push_str(" -C target-feature=+crt-static"); - } - if build_remote_server.contains("mold") { - rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); - } - - if self.ssh_platform.arch == std::env::consts::ARCH - && self.ssh_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", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; - - 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); - - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string(); - #[cfg(not(target_os = "windows"))] - let src = "./target"; - run_cmd( - Command::new("cross") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env( - "CROSS_CONTAINER_OPTS", - format!("--mount type=bind,src={src},dst=/app/target"), - ) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - let which = cx - .background_spawn(async move { which::which("zig") }) - .await; - - if which.is_err() { - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - #[cfg(target_os = "windows")] - { - anyhow::bail!( - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - } - - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; - - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; - - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - Command::new("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - }; - let bin_path = Path::new("target") - .join("remote_server") - .join(&triple) - .join("debug") - .join("remote_server"); - - let path = if !build_remote_server.contains("nocompress") { - delegate.set_status(Some("Compressing binary"), cx); - - #[cfg(not(target_os = "windows"))] - { - run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; - } - #[cfg(target_os = "windows")] - { - // On Windows, we use 7z to compress the binary - let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; - let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); - if smol::fs::metadata(&gz_path).await.is_ok() { - smol::fs::remove_file(&gz_path).await?; - } - run_cmd(Command::new(seven_zip).args([ - "a", - "-tgzip", - &gz_path, - &bin_path.to_string_lossy(), - ])) - .await?; - } - - let mut archive_path = bin_path; - archive_path.set_extension("gz"); - std::env::current_dir()?.join(archive_path) - } else { - bin_path - }; - - Ok(path) - } } impl SshSocket { diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea8f2443d9a674492674bdc2fb19f2a021b03dcc --- /dev/null +++ b/crates/remote/src/transport/wsl.rs @@ -0,0 +1,494 @@ +use crate::{ + RemoteClientDelegate, RemotePlatform, + remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, +}; +use anyhow::{Result, anyhow, bail}; +use async_trait::async_trait; +use collections::HashMap; +use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; +use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task}; +use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use rpc::proto::Envelope; +use smol::{fs, process}; +use std::{ + fmt::Write as _, + path::{Path, PathBuf}, + process::Stdio, + sync::Arc, + time::Instant, +}; +use util::paths::{PathStyle, RemotePathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WslConnectionOptions { + pub distro_name: String, + pub user: Option, +} + +pub(crate) struct WslRemoteConnection { + remote_binary_path: Option, + platform: RemotePlatform, + shell: String, + connection_options: WslConnectionOptions, +} + +impl WslRemoteConnection { + pub(crate) async fn new( + connection_options: WslConnectionOptions, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Result { + log::info!( + "Connecting to WSL distro {} with user {:?}", + connection_options.distro_name, + connection_options.user + ); + let (release_channel, version, commit) = cx.update(|cx| { + ( + ReleaseChannel::global(cx), + AppVersion::global(cx), + AppCommitSha::try_global(cx), + ) + })?; + + let mut this = Self { + connection_options, + remote_binary_path: None, + platform: RemotePlatform { os: "", arch: "" }, + shell: String::new(), + }; + delegate.set_status(Some("Detecting WSL environment"), cx); + this.platform = this.detect_platform().await?; + this.shell = this.detect_shell().await?; + this.remote_binary_path = Some( + this.ensure_server_binary(&delegate, release_channel, version, commit, cx) + .await?, + ); + + Ok(this) + } + + async fn detect_platform(&self) -> Result { + let arch_str = self.run_wsl_command("uname", &["-m"]).await?; + let arch_str = arch_str.trim().to_string(); + let arch = match arch_str.as_str() { + "x86_64" => "x86_64", + "aarch64" | "arm64" => "aarch64", + _ => "x86_64", + }; + Ok(RemotePlatform { os: "linux", arch }) + } + + async fn detect_shell(&self) -> Result { + Ok(self + .run_wsl_command("sh", &["-c", "echo $SHELL"]) + .await + .ok() + .and_then(|shell_path| shell_path.trim().split('/').next_back().map(str::to_string)) + .unwrap_or_else(|| "bash".to_string())) + } + + async fn windows_path_to_wsl_path(&self, source: &Path) -> Result { + windows_path_to_wsl_path_impl(&self.connection_options, source).await + } + + fn wsl_command(&self, program: &str, args: &[&str]) -> process::Command { + wsl_command_impl(&self.connection_options, program, args) + } + + async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result { + run_wsl_command_impl(&self.connection_options, program, args).await + } + + async fn ensure_server_binary( + &self, + delegate: &Arc, + release_channel: ReleaseChannel, + version: SemanticVersion, + commit: Option, + cx: &mut AsyncApp, + ) -> Result { + let version_str = match release_channel { + ReleaseChannel::Nightly => { + let commit = commit.map(|s| s.full()).unwrap_or_default(); + format!("{}-{}", version, commit) + } + ReleaseChannel::Dev => "build".to_string(), + _ => version.to_string(), + }; + + let binary_name = format!( + "zed-remote-server-{}-{}", + release_channel.dev_name(), + version_str + ); + + let dst_path = RemotePathBuf::new( + paths::remote_wsl_server_dir_relative().join(binary_name), + PathStyle::Posix, + ); + + if let Some(parent) = dst_path.parent() { + self.run_wsl_command("mkdir", &["-p", &parent.to_string()]) + .await + .map_err(|e| anyhow!("Failed to create directory: {}", e))?; + } + + #[cfg(debug_assertions)] + if let Some(remote_server_path) = + super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await? + { + let tmp_path = RemotePathBuf::new( + paths::remote_wsl_server_dir_relative().join(format!( + "download-{}-{}", + std::process::id(), + remote_server_path.file_name().unwrap().to_string_lossy() + )), + PathStyle::Posix, + ); + self.upload_file(&remote_server_path, &tmp_path, delegate, cx) + .await?; + self.extract_and_install(&tmp_path, &dst_path, delegate, cx) + .await?; + return Ok(dst_path); + } + + if self + .run_wsl_command(&dst_path.to_string(), &["version"]) + .await + .is_ok() + { + return Ok(dst_path); + } + + delegate.set_status(Some("Installing remote server"), cx); + + let wanted_version = match release_channel { + ReleaseChannel::Nightly => None, + ReleaseChannel::Dev => { + return Err(anyhow!("Dev builds require manual installation")); + } + _ => Some(cx.update(|cx| AppVersion::global(cx))?), + }; + + let src_path = delegate + .download_server_binary_locally(self.platform, release_channel, wanted_version, cx) + .await?; + + let tmp_path = RemotePathBuf::new( + PathBuf::from(format!("{}.{}.tmp", dst_path, std::process::id())), + PathStyle::Posix, + ); + + self.upload_file(&src_path, &tmp_path, delegate, cx).await?; + self.extract_and_install(&tmp_path, &dst_path, delegate, cx) + .await?; + + Ok(dst_path) + } + + async fn upload_file( + &self, + src_path: &Path, + dst_path: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + delegate.set_status(Some("Uploading remote server to WSL"), cx); + + if let Some(parent) = dst_path.parent() { + self.run_wsl_command("mkdir", &["-p", &parent.to_string()]) + .await + .map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?; + } + + let t0 = Instant::now(); + let src_stat = fs::metadata(&src_path).await?; + let size = src_stat.len(); + log::info!( + "uploading remote server to WSL {:?} ({}kb)", + dst_path, + size / 1024 + ); + + let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?; + self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()]) + .await + .map_err(|e| { + anyhow!( + "Failed to copy file {}({}) to WSL {:?}: {}", + src_path.display(), + src_path_in_wsl, + dst_path, + e + ) + })?; + + log::info!("uploaded remote server in {:?}", t0.elapsed()); + Ok(()) + } + + async fn extract_and_install( + &self, + tmp_path: &RemotePathBuf, + dst_path: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + delegate.set_status(Some("Extracting remote server"), cx); + + let tmp_path_str = tmp_path.to_string(); + let dst_path_str = dst_path.to_string(); + + // Build extraction script with proper error handling + let script = if tmp_path_str.ends_with(".gz") { + let uncompressed = tmp_path_str.trim_end_matches(".gz"); + format!( + "set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'", + tmp_path_str, uncompressed, uncompressed, dst_path_str + ) + } else { + format!( + "set -e; chmod 755 '{}' && mv -f '{}' '{}'", + tmp_path_str, tmp_path_str, dst_path_str + ) + }; + + self.run_wsl_command("sh", &["-c", &script]) + .await + .map_err(|e| anyhow!("Failed to extract server binary: {}", e))?; + Ok(()) + } +} + +#[async_trait(?Send)] +impl RemoteConnection for WslRemoteConnection { + fn start_proxy( + &self, + unique_identifier: String, + reconnect: bool, + incoming_tx: UnboundedSender, + outgoing_rx: UnboundedReceiver, + connection_activity_tx: Sender<()>, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Task> { + delegate.set_status(Some("Starting proxy"), cx); + + let Some(remote_binary_path) = &self.remote_binary_path else { + return Task::ready(Err(anyhow!("Remote binary path not set"))); + }; + + let mut proxy_command = format!( + "exec {} proxy --identifier {}", + remote_binary_path, unique_identifier + ); + + if reconnect { + proxy_command.push_str(" --reconnect"); + } + + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + proxy_command = format!("{}='{}' {}", env_var, value, proxy_command); + } + } + let proxy_process = match self + .wsl_command("sh", &["-lc", &proxy_command]) + .kill_on_drop(true) + .spawn() + { + Ok(process) => process, + Err(error) => { + return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); + } + }; + + super::handle_rpc_messages_over_child_process_stdio( + proxy_process, + incoming_tx, + outgoing_rx, + connection_activity_tx, + cx, + ) + } + + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task> { + cx.background_spawn({ + let options = self.connection_options.clone(); + async move { + let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?; + + run_wsl_command_impl(&options, "cp", &["-r", &wsl_src, &dest_path.to_string()]) + .await + .map_err(|e| { + anyhow!( + "failed to upload directory {} -> {}: {}", + src_path.display(), + dest_path.to_string(), + e + ) + })?; + + Ok(()) + } + }) + } + + async fn kill(&self) -> Result<()> { + Ok(()) + } + + fn has_been_killed(&self) -> bool { + false + } + + fn shares_network_interface(&self) -> bool { + true + } + + fn build_command( + &self, + program: Option, + args: &[String], + env: &HashMap, + working_dir: Option, + port_forward: Option<(u16, String, u16)>, + ) -> Result { + if port_forward.is_some() { + bail!("WSL shares the network interface with the host system"); + } + + let working_dir = working_dir + .map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string()) + .unwrap_or("~".to_string()); + + let mut script = String::new(); + + for (k, v) in env.iter() { + write!(&mut script, "{}='{}' ", k, v).unwrap(); + } + + if let Some(program) = program { + let command = shlex::try_quote(&program)?; + script.push_str(&command); + for arg in args { + let arg = shlex::try_quote(&arg)?; + script.push_str(" "); + script.push_str(&arg); + } + } else { + write!(&mut script, "exec {} -l", self.shell).unwrap(); + } + + let wsl_args = if let Some(user) = &self.connection_options.user { + vec![ + "--distribution".to_string(), + self.connection_options.distro_name.clone(), + "--user".to_string(), + user.clone(), + "--cd".to_string(), + working_dir, + "--".to_string(), + self.shell.clone(), + "-c".to_string(), + shlex::try_quote(&script)?.to_string(), + ] + } else { + vec![ + "--distribution".to_string(), + self.connection_options.distro_name.clone(), + "--cd".to_string(), + working_dir, + "--".to_string(), + self.shell.clone(), + "-c".to_string(), + shlex::try_quote(&script)?.to_string(), + ] + }; + + Ok(CommandTemplate { + program: "wsl.exe".to_string(), + args: wsl_args, + env: HashMap::default(), + }) + } + + fn connection_options(&self) -> RemoteConnectionOptions { + RemoteConnectionOptions::Wsl(self.connection_options.clone()) + } + + fn path_style(&self) -> PathStyle { + PathStyle::Posix + } + + fn shell(&self) -> String { + self.shell.clone() + } +} + +/// `wslpath` is a executable available in WSL, it's a linux binary. +/// So it doesn't support Windows style paths. +async fn sanitize_path(path: &Path) -> Result { + let path = smol::fs::canonicalize(path).await?; + let path_str = path.to_string_lossy(); + + let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str); + Ok(sanitized.replace('\\', "/")) +} + +async fn windows_path_to_wsl_path_impl( + options: &WslConnectionOptions, + source: &Path, +) -> Result { + let source = sanitize_path(source).await?; + run_wsl_command_impl(options, "wslpath", &["-u", &source]).await +} + +fn wsl_command_impl( + options: &WslConnectionOptions, + program: &str, + args: &[&str], +) -> process::Command { + let mut command = util::command::new_smol_command("wsl.exe"); + + if let Some(user) = &options.user { + command.arg("--user").arg(user); + } + + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg("--distribution") + .arg(&options.distro_name) + .arg("--cd") + .arg("~") + .arg(program) + .args(args); + + command +} + +async fn run_wsl_command_impl( + options: &WslConnectionOptions, + program: &str, + args: &[&str], +) -> Result { + let output = wsl_command_impl(options, program, args).output().await?; + + if !output.status.success() { + return Err(anyhow!( + "Command '{}' failed: {}", + program, + String::from_utf8_lossy(&output.stderr).trim() + )); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 075b9fcd86276244d154be1aebe904fbfb4a7b6c..2b13ef58c3a8707b81d6870590efe5337ffef048 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -32,6 +32,7 @@ use gpui::{ use keymap_editor; use onboarding_banner::OnboardingBanner; use project::Project; +use remote::RemoteConnectionOptions; use settings::Settings as _; use std::sync::Arc; use theme::ActiveTheme; @@ -304,12 +305,14 @@ impl TitleBar { fn render_remote_project_connection(&self, cx: &mut Context) -> Option { let options = self.project.read(cx).remote_connection_options(cx)?; - let host: SharedString = options.connection_string().into(); + let host: SharedString = options.display_name().into(); - let nickname = options - .nickname - .map(|nick| nick.into()) - .unwrap_or_else(|| host.clone()); + let nickname = if let RemoteConnectionOptions::Ssh(options) = options { + options.nickname.map(|nick| nick.into()) + } else { + None + }; + let nickname = nickname.unwrap_or_else(|| host.clone()); let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? { remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")), diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 3ef9ff65eb0fe5aedfd5e72aa18f1481a011fce7..160823f547f3ab0019d4a631550aec70f1ca101e 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -20,6 +20,7 @@ use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; +use remote::{RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -33,11 +34,12 @@ use uuid::Uuid; use crate::{ WorkspaceId, path_list::{PathList, SerializedPathList}, + persistence::model::RemoteConnectionKind, }; use model::{ - GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, SshConnectionId, + GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane, + SerializedPaneGroup, SerializedWorkspace, }; use self::model::{DockStructure, SerializedWorkspaceLocation}; @@ -627,6 +629,88 @@ impl Domain for WorkspaceDb { END WHERE paths IS NOT NULL ), + sql!( + CREATE TABLE remote_connections( + id INTEGER PRIMARY KEY, + kind TEXT NOT NULL, + host TEXT, + port INTEGER, + user TEXT, + distro TEXT + ); + + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + remote_connection_id INTEGER REFERENCES remote_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; + + INSERT INTO remote_connections + SELECT + id, + "ssh" as kind, + host, + port, + user, + NULL as distro + FROM ssh_connections; + + INSERT + INTO workspaces_2 + SELECT + workspace_id, + paths, + paths_order, + ssh_connection_id as remote_connection_id, + timestamp, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + left_dock_visible, + left_dock_active_panel, + right_dock_visible, + right_dock_active_panel, + bottom_dock_visible, + bottom_dock_active_panel, + left_dock_zoom, + right_dock_zoom, + bottom_dock_zoom, + fullscreen, + centered_layout, + session_id, + window_id + FROM + workspaces; + + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths); + ), ]; // Allow recovering from bad migration that was initially shipped to nightly @@ -650,10 +734,10 @@ impl WorkspaceDb { self.workspace_for_roots_internal(worktree_roots, None) } - pub(crate) fn ssh_workspace_for_roots>( + pub(crate) fn remote_workspace_for_roots>( &self, worktree_roots: &[P], - ssh_project_id: SshConnectionId, + ssh_project_id: RemoteConnectionId, ) -> Option { self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) } @@ -661,7 +745,7 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots_internal>( &self, worktree_roots: &[P], - ssh_connection_id: Option, + remote_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces @@ -713,13 +797,13 @@ impl WorkspaceDb { FROM workspaces WHERE paths IS ? AND - ssh_connection_id IS ? + remote_connection_id IS ? LIMIT 1 }) .map(|mut prepared_statement| { (prepared_statement)(( root_paths.serialize().paths, - ssh_connection_id.map(|id| id.0 as i32), + remote_connection_id.map(|id| id.0 as i32), )) .unwrap() }) @@ -803,14 +887,12 @@ impl WorkspaceDb { log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { - let ssh_connection_id = match &workspace.location { + let remote_connection_id = match workspace.location.clone() { SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(connection) => { - Some(Self::get_or_create_ssh_connection_query( + SerializedWorkspaceLocation::Remote(connection_options) => { + Some(Self::get_or_create_remote_connection_internal( conn, - connection.host.clone(), - connection.port, - connection.user.clone(), + connection_options )?.0) } }; @@ -860,11 +942,11 @@ impl WorkspaceDb { WHERE workspace_id != ?1 AND paths IS ?2 AND - ssh_connection_id IS ?3 + remote_connection_id IS ?3 ))?(( workspace.id, paths.paths.clone(), - ssh_connection_id, + remote_connection_id, )) .context("clearing out old locations")?; @@ -874,7 +956,7 @@ impl WorkspaceDb { workspace_id, paths, paths_order, - ssh_connection_id, + remote_connection_id, left_dock_visible, left_dock_active_panel, left_dock_zoom, @@ -893,7 +975,7 @@ impl WorkspaceDb { UPDATE SET paths = ?2, paths_order = ?3, - ssh_connection_id = ?4, + remote_connection_id = ?4, left_dock_visible = ?5, left_dock_active_panel = ?6, left_dock_zoom = ?7, @@ -912,7 +994,7 @@ impl WorkspaceDb { workspace.id, paths.paths.clone(), paths.order.clone(), - ssh_connection_id, + remote_connection_id, workspace.docks, workspace.session_id, workspace.window_id, @@ -931,39 +1013,78 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_connection( + pub(crate) async fn get_or_create_remote_connection( &self, - host: String, - port: Option, - user: Option, - ) -> Result { - self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) + options: RemoteConnectionOptions, + ) -> Result { + self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options)) .await } - fn get_or_create_ssh_connection_query( + fn get_or_create_remote_connection_internal( + this: &Connection, + options: RemoteConnectionOptions, + ) -> Result { + let kind; + let user; + let mut host = None; + let mut port = None; + let mut distro = None; + match options { + RemoteConnectionOptions::Ssh(options) => { + kind = RemoteConnectionKind::Ssh; + host = Some(options.host); + port = options.port; + user = options.username; + } + RemoteConnectionOptions::Wsl(options) => { + kind = RemoteConnectionKind::Wsl; + distro = Some(options.distro_name); + user = options.user; + } + } + Self::get_or_create_remote_connection_query(this, kind, host, port, user, distro) + } + + fn get_or_create_remote_connection_query( this: &Connection, - host: String, + kind: RemoteConnectionKind, + host: Option, port: Option, user: Option, - ) -> Result { + distro: Option, + ) -> Result { if let Some(id) = this.select_row_bound(sql!( - SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 - ))?((host.clone(), port, user.clone()))? - { - Ok(SshConnectionId(id)) + SELECT id + FROM remote_connections + WHERE + kind IS ? AND + host IS ? AND + port IS ? AND + user IS ? AND + distro IS ? + LIMIT 1 + ))?(( + kind.serialize(), + host.clone(), + port, + user.clone(), + distro.clone(), + ))? { + Ok(RemoteConnectionId(id)) } else { - log::debug!("Inserting SSH project at host {host}"); let id = this.select_row_bound(sql!( - INSERT INTO ssh_connections ( + INSERT INTO remote_connections ( + kind, host, port, - user - ) VALUES (?1, ?2, ?3) + user, + distro + ) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id - ))?((host, port, user))? - .context("failed to insert ssh project")?; - Ok(SshConnectionId(id)) + ))?((kind.serialize(), host, port, user, distro))? + .context("failed to insert remote project")?; + Ok(RemoteConnectionId(id)) } } @@ -973,15 +1094,17 @@ impl WorkspaceDb { } } - fn recent_workspaces(&self) -> Result)>> { + fn recent_workspaces( + &self, + ) -> Result)>> { Ok(self .recent_workspaces_query()? .into_iter() - .map(|(id, paths, order, ssh_connection_id)| { + .map(|(id, paths, order, remote_connection_id)| { ( id, PathList::deserialize(&SerializedPathList { paths, order }), - ssh_connection_id, + remote_connection_id.map(RemoteConnectionId), ) }) .collect()) @@ -1001,7 +1124,7 @@ impl WorkspaceDb { fn session_workspaces( &self, session_id: String, - ) -> Result, Option)>> { + ) -> Result, Option)>> { Ok(self .session_workspaces_query(session_id)? .into_iter() @@ -1009,7 +1132,7 @@ impl WorkspaceDb { ( PathList::deserialize(&SerializedPathList { paths, order }), window_id, - ssh_connection_id.map(SshConnectionId), + ssh_connection_id.map(RemoteConnectionId), ) }) .collect()) @@ -1017,7 +1140,7 @@ impl WorkspaceDb { query! { fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, ssh_connection_id + SELECT paths, paths_order, window_id, remote_connection_id FROM workspaces WHERE session_id = ?1 ORDER BY timestamp DESC @@ -1039,40 +1162,55 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { - Ok(self - .ssh_connections_query()? - .into_iter() - .map(|(id, host, port, user)| { - ( - SshConnectionId(id), - SerializedSshConnection { host, port, user }, - ) - }) - .collect()) - } - - query! { - pub fn ssh_connections_query() -> Result, Option)>> { - SELECT id, host, port, user - FROM ssh_connections - } - } - - pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { - let row = self.ssh_connection_query(id.0)?; - Ok(SerializedSshConnection { - host: row.0, - port: row.1, - user: row.2, + fn remote_connections(&self) -> Result> { + Ok(self.select(sql!( + SELECT + id, kind, host, port, user, distro + FROM + remote_connections + ))?()? + .into_iter() + .filter_map(|(id, kind, host, port, user, distro)| { + Some(( + RemoteConnectionId(id), + Self::remote_connection_from_row(kind, host, port, user, distro)?, + )) }) + .collect()) } - query! { - fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { - SELECT host, port, user - FROM ssh_connections + pub(crate) fn remote_connection( + &self, + id: RemoteConnectionId, + ) -> Result { + let (kind, host, port, user, distro) = self.select_row_bound(sql!( + SELECT kind, host, port, user, distro + FROM remote_connections WHERE id = ? + ))?(id.0)? + .context("no such remote connection")?; + Self::remote_connection_from_row(kind, host, port, user, distro) + .context("invalid remote_connection row") + } + + fn remote_connection_from_row( + kind: String, + host: Option, + port: Option, + user: Option, + distro: Option, + ) -> Option { + match RemoteConnectionKind::deserialize(&kind)? { + RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { + distro_name: distro?, + user: user, + })), + RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host?, + port, + username: user, + ..Default::default() + })), } } @@ -1108,14 +1246,14 @@ impl WorkspaceDb { ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_connections = self.ssh_connections()?; + let remote_connections = self.remote_connections()?; - for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { - if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { + for (id, paths, remote_connection_id) in self.recent_workspaces()? { + if let Some(remote_connection_id) = remote_connection_id { + if let Some(connection_options) = remote_connections.get(&remote_connection_id) { result.push(( id, - SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), + SerializedWorkspaceLocation::Remote(connection_options.clone()), paths, )); } else { @@ -1157,12 +1295,14 @@ impl WorkspaceDb { ) -> Result> { let mut workspaces = Vec::new(); - for (paths, window_id, ssh_connection_id) in + for (paths, window_id, remote_connection_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_connection_id) = ssh_connection_id { + if let Some(remote_connection_id) = remote_connection_id { workspaces.push(( - SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), + SerializedWorkspaceLocation::Remote( + self.remote_connection(remote_connection_id)?, + ), paths, window_id.map(WindowId::from), )); @@ -1545,6 +1685,7 @@ mod tests { }; use gpui; use pretty_assertions::assert_eq; + use remote::SshConnectionOptions; use std::{thread, time::Duration}; #[gpui::test] @@ -2196,14 +2337,20 @@ mod tests { }; let connection_id = db - .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: "my-host".to_string(), + port: Some(1234), + ..Default::default() + })) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), + location: SerializedWorkspaceLocation::Remote( + db.remote_connection(connection_id).unwrap(), + ), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2362,13 +2509,12 @@ mod tests { } #[gpui::test] - async fn test_last_session_workspace_locations_ssh_projects() { - let db = WorkspaceDb::open_test_db( - "test_serializing_workspaces_last_session_workspaces_ssh_projects", - ) - .await; + async fn test_last_session_workspace_locations_remote() { + let db = + WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote") + .await; - let ssh_connections = [ + let remote_connections = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2376,30 +2522,31 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + let options = RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.to_string(), + username: Some(user.to_string()), + ..Default::default() + }); + db.get_or_create_remote_connection(options.clone()) .await .unwrap(); - SerializedSshConnection { - host: host.into(), - port: None, - user: Some(user.into()), - } + options }) .collect::>(); - let ssh_connections = futures::future::join_all(ssh_connections).await; + let remote_connections = futures::future::join_all(remote_connections).await; let workspaces = [ - (1, ssh_connections[0].clone(), 9), - (2, ssh_connections[1].clone(), 5), - (3, ssh_connections[2].clone(), 8), - (4, ssh_connections[3].clone(), 2), + (1, remote_connections[0].clone(), 9), + (2, remote_connections[1].clone(), 5), + (3, remote_connections[2].clone(), 8), + (4, remote_connections[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_connection, window_id)| SerializedWorkspace { + .map(|(id, remote_connection, window_id)| SerializedWorkspace { id: WorkspaceId(id), paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(ssh_connection), + location: SerializedWorkspaceLocation::Remote(remote_connection), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2429,28 +2576,28 @@ mod tests { assert_eq!( have[0], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), PathList::default() ) ); assert_eq!( have[1], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), PathList::default() ) ); assert_eq!( have[2], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), PathList::default() ) ); assert_eq!( have[3], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), PathList::default() ) ); @@ -2465,13 +2612,23 @@ mod tests { let user = Some("user".to_string()); let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: user.clone(), + ..Default::default() + })) .await .unwrap(); // Test that calling the function again with the same parameters returns the same project let same_connection = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: user.clone(), + ..Default::default() + })) .await .unwrap(); @@ -2483,7 +2640,12 @@ mod tests { let user2 = Some("otheruser".to_string()); let different_connection = db - .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host2.clone(), + port: port2, + username: user2.clone(), + ..Default::default() + })) .await .unwrap(); @@ -2497,12 +2659,22 @@ mod tests { let (host, port, user) = ("example.com".to_string(), None, None); let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, None) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: None, + ..Default::default() + })) .await .unwrap(); let same_connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: user.clone(), + ..Default::default() + })) .await .unwrap(); @@ -2510,8 +2682,8 @@ mod tests { } #[gpui::test] - async fn test_get_ssh_connections() { - let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; + async fn test_get_remote_connections() { + let db = WorkspaceDb::open_test_db("test_get_remote_connections").await; let connections = [ ("example.com".to_string(), None, None), @@ -2526,39 +2698,49 @@ mod tests { let mut ids = Vec::new(); for (host, port, user) in connections.iter() { ids.push( - db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) - .await - .unwrap(), + db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh( + SshConnectionOptions { + host: host.clone(), + port: *port, + username: user.clone(), + ..Default::default() + }, + )) + .await + .unwrap(), ); } - let stored_projects = db.ssh_connections().unwrap(); + let stored_connections = db.remote_connections().unwrap(); assert_eq!( - stored_projects, + stored_connections, [ ( ids[0], - SerializedSshConnection { + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "example.com".into(), port: None, - user: None, - } + username: None, + ..Default::default() + }), ), ( ids[1], - SerializedSshConnection { + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "anotherexample.com".into(), port: Some(123), - user: Some("user2".into()), - } + username: Some("user2".into()), + ..Default::default() + }), ), ( ids[2], - SerializedSshConnection { + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "yetanother.com".into(), port: Some(345), - user: None, - } + username: None, + ..Default::default() + }), ), ] .into_iter() diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 04757d04950ac1ca200096d7b46d04abb18ce8f9..005a1ba2347f8ac3847199ad4564d8ca45420f4a 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -12,7 +12,7 @@ use db::sqlez::{ use gpui::{AsyncWindowContext, Entity, WeakEntity}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; -use serde::{Deserialize, Serialize}; +use remote::RemoteConnectionOptions; use std::{ collections::BTreeMap, path::{Path, PathBuf}, @@ -24,19 +24,18 @@ use uuid::Uuid; #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, )] -pub(crate) struct SshConnectionId(pub u64); +pub(crate) struct RemoteConnectionId(pub u64); -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshConnection { - pub host: String, - pub port: Option, - pub user: Option, +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum RemoteConnectionKind { + Ssh, + Wsl, } #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { Local, - Ssh(SerializedSshConnection), + Remote(RemoteConnectionOptions), } impl SerializedWorkspaceLocation { @@ -68,6 +67,23 @@ pub struct DockStructure { pub(crate) bottom: DockData, } +impl RemoteConnectionKind { + pub(crate) fn serialize(&self) -> &'static str { + match self { + RemoteConnectionKind::Ssh => "ssh", + RemoteConnectionKind::Wsl => "wsl", + } + } + + pub(crate) fn deserialize(text: &str) -> Option { + match text { + "ssh" => Some(Self::Ssh), + "wsl" => Some(Self::Wsl), + _ => None, + } + } +} + impl Column for DockStructure { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let (left, next_index) = DockData::column(statement, start_index)?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 61442eb6348e6152a4ad8ba4d3f93c24d1887346..bd19f37c1e0fd8653f5d73dea365f1148fd2e91d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -67,14 +67,14 @@ pub use pane_group::*; use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, + model::{ItemId, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier}; +use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -5262,14 +5262,7 @@ impl Workspace { fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { let paths = PathList::new(&self.root_paths(cx)); if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { - WorkspaceLocation::Location( - SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - host: connection.host, - port: connection.port, - user: connection.username, - }), - paths, - ) + WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths) } else if self.project.read(cx).is_local() { if !paths.is_empty() { WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) @@ -7282,9 +7275,9 @@ pub fn create_and_open_local_file( }) } -pub fn open_ssh_project_with_new_connection( +pub fn open_remote_project_with_new_connection( window: WindowHandle, - connection_options: SshConnectionOptions, + connection_options: RemoteConnectionOptions, cancel_rx: oneshot::Receiver<()>, delegate: Arc, app_state: Arc, @@ -7293,11 +7286,11 @@ pub fn open_ssh_project_with_new_connection( ) -> Task> { cx.spawn(async move |cx| { let (workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; + serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx .update(|cx| { - remote::RemoteClient::ssh( + remote::RemoteClient::new( ConnectionIdentifier::Workspace(workspace_id.0), connection_options, cancel_rx, @@ -7323,7 +7316,7 @@ pub fn open_ssh_project_with_new_connection( ) })?; - open_ssh_project_inner( + open_remote_project_inner( project, paths, workspace_id, @@ -7336,8 +7329,8 @@ pub fn open_ssh_project_with_new_connection( }) } -pub fn open_ssh_project_with_existing_connection( - connection_options: SshConnectionOptions, +pub fn open_remote_project_with_existing_connection( + connection_options: RemoteConnectionOptions, project: Entity, paths: Vec, app_state: Arc, @@ -7346,9 +7339,9 @@ pub fn open_ssh_project_with_existing_connection( ) -> Task> { cx.spawn(async move |cx| { let (workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; + serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; - open_ssh_project_inner( + open_remote_project_inner( project, paths, workspace_id, @@ -7361,7 +7354,7 @@ pub fn open_ssh_project_with_existing_connection( }) } -async fn open_ssh_project_inner( +async fn open_remote_project_inner( project: Entity, paths: Vec, workspace_id: WorkspaceId, @@ -7448,22 +7441,18 @@ async fn open_ssh_project_inner( Ok(()) } -fn serialize_ssh_project( - connection_options: SshConnectionOptions, +fn serialize_remote_project( + connection_options: RemoteConnectionOptions, paths: Vec, cx: &AsyncApp, ) -> Task)>> { cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection( - connection_options.host.clone(), - connection_options.port, - connection_options.username.clone(), - ) + let remote_connection_id = persistence::DB + .get_or_create_remote_connection(connection_options) .await?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -8013,22 +8002,20 @@ pub struct WorkspacePosition { pub centered_layout: bool, } -pub fn ssh_workspace_position_from_db( - host: String, - port: Option, - user: Option, +pub fn remote_workspace_position_from_db( + connection_options: RemoteConnectionOptions, paths_to_open: &[PathBuf], cx: &App, ) -> Task> { let paths = paths_to_open.to_vec(); cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection(host, port, user) + let remote_connection_id = persistence::DB + .get_or_create_remote_connection(connection_options) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/zed/resources/windows/zed-wsl b/crates/zed/resources/windows/zed-wsl new file mode 100644 index 0000000000000000000000000000000000000000..d3cbb93af6f5979508229656deadeab0dbf21661 --- /dev/null +++ b/crates/zed/resources/windows/zed-wsl @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +if [ "$ZED_WSL_DEBUG_INFO" = true ]; then + set -x +fi + +ZED_PATH="$(dirname "$(realpath "$0")")" + +IN_WSL=false +if [ -n "$WSL_DISTRO_NAME" ]; then + # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 + IN_WSL=true +fi + +if [ $IN_WSL = true ]; then + WSL_USER="$USER" + if [ -z "$WSL_USER" ]; then + WSL_USER="$USERNAME" + fi + "$ZED_PATH/zed.exe" --wsl "$WSL_USER@$WSL_DISTRO_NAME" "$@" + exit $? +else + echo "Only WSL is supported for now" >&2 + exit 1 +fi diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e4438792045617498e5c8cd3b52117b1d0b752ef..79cf2bfa66fb217680dea86720eb46402f116958 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -23,13 +23,14 @@ use http_client::{Url, read_proxy_from_env}; use language::LanguageRegistry; use onboarding::{FIRST_OPEN, show_onboarding_view}; use prompt_store::PromptBuilder; +use remote::RemoteConnectionOptions; use reqwest_client::ReqwestClient; use assets::Assets; use node_runtime::{NodeBinaryOptions, NodeRuntime}; use parking_lot::Mutex; use project::project_settings::ProjectSettings; -use recent_projects::{SshSettings, open_ssh_project}; +use recent_projects::{SshSettings, open_remote_project}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; @@ -360,6 +361,7 @@ pub fn main() { open_listener.open(RawOpenRequest { urls, diff_paths: Vec::new(), + ..Default::default() }) } }); @@ -696,7 +698,7 @@ pub fn main() { let urls: Vec<_> = args .paths_or_urls .iter() - .filter_map(|arg| parse_url_arg(arg, cx).log_err()) + .map(|arg| parse_url_arg(arg, cx)) .collect(); let diff_paths: Vec<[String; 2]> = args @@ -706,7 +708,11 @@ pub fn main() { .collect(); if !urls.is_empty() || !diff_paths.is_empty() { - open_listener.open(RawOpenRequest { urls, diff_paths }) + open_listener.open(RawOpenRequest { + urls, + diff_paths, + wsl: args.wsl, + }) } match open_rx @@ -792,10 +798,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut return; } - if let Some(connection_options) = request.ssh_connection { + if let Some(connection_options) = request.remote_connection { cx.spawn(async move |cx| { let paths: Vec = request.open_paths.into_iter().map(PathBuf::from).collect(); - open_ssh_project( + open_remote_project( connection_options, paths, app_state, @@ -978,31 +984,24 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp tasks.push(task); } } - SerializedWorkspaceLocation::Ssh(ssh) => { + SerializedWorkspaceLocation::Remote(mut connection_options) => { let app_state = app_state.clone(); - let ssh_host = ssh.host.clone(); - let task = cx.spawn(async move |cx| { - let connection_options = cx.update(|cx| { + if let RemoteConnectionOptions::Ssh(options) = &mut connection_options { + cx.update(|cx| { SshSettings::get_global(cx) - .connection_options_for(ssh.host, ssh.port, ssh.user) - }); - - match connection_options { - Ok(connection_options) => recent_projects::open_ssh_project( - connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - .map_err(|e| anyhow::anyhow!(e)), - Err(e) => Err(anyhow::anyhow!( - "Failed to get SSH connection options for {}: {}", - ssh_host, - e - )), - } + .fill_connection_options_from_settings(options) + })?; + } + let task = cx.spawn(async move |cx| { + recent_projects::open_remote_project( + connection_options, + paths.paths().into_iter().map(PathBuf::from).collect(), + app_state, + workspace::OpenOptions::default(), + cx, + ) + .await + .map_err(|e| anyhow::anyhow!(e)) }); tasks.push(task); } @@ -1184,6 +1183,16 @@ struct Args { #[arg(long, value_name = "DIR")] user_data_dir: Option, + /// The username and WSL distribution to use when opening paths. ,If not specified, + /// Zed will attempt to open the paths directly. + /// + /// The username is optional, and if not specified, the default user for the distribution + /// will be used. + /// + /// Example: `me@Ubuntu` or `Ubuntu` for default distribution. + #[arg(long, value_name = "USER@DISTRO")] + wsl: Option, + /// Instructs zed to run as a dev server on this machine. (not implemented) #[arg(long)] dev_server_token: Option, @@ -1242,18 +1251,18 @@ impl ToString for IdType { } } -fn parse_url_arg(arg: &str, cx: &App) -> Result { +fn parse_url_arg(arg: &str, cx: &App) -> String { match std::fs::canonicalize(Path::new(&arg)) { - Ok(path) => Ok(format!("file://{}", path.display())), - Err(error) => { + Ok(path) => format!("file://{}", path.display()), + Err(_) => { if arg.starts_with("file://") || arg.starts_with("zed-cli://") || arg.starts_with("ssh://") || parse_zed_link(arg, cx).is_some() { - Ok(arg.into()) + arg.into() } else { - anyhow::bail!("error parsing path argument: {error}") + format!("file://{arg}") } } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5797070a39c8a60dc760ac3b82341842bc11d63e..d0e4687a132a85645cdbfe52e67ebb6afd894c0e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -48,7 +48,7 @@ use project::{DirectoryLister, ProjectItem}; use project_panel::ProjectPanel; use prompt_store::PromptBuilder; use quick_action_bar::QuickActionBar; -use recent_projects::open_ssh_project; +use recent_projects::open_remote_project; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; @@ -1557,7 +1557,7 @@ pub fn open_new_ssh_project_from_project( }; let connection_options = ssh_client.read(cx).connection_options(); cx.spawn_in(window, async move |_, cx| { - open_ssh_project( + open_remote_project( connection_options, paths, app_state, diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2194fb7af5d48577a4316b99418df7dbce0a0375..f2d8cd46c301c0f688d36e17ed1b7d0dcd31ec00 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -17,8 +17,8 @@ use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; use onboarding::FIRST_OPEN; use onboarding::show_onboarding_view; -use recent_projects::{SshSettings, open_ssh_project}; -use remote::SshConnectionOptions; +use recent_projects::{SshSettings, open_remote_project}; +use remote::{RemoteConnectionOptions, WslConnectionOptions}; use settings::Settings; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -37,7 +37,7 @@ pub struct OpenRequest { pub diff_paths: Vec<[String; 2]>, pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, - pub ssh_connection: Option, + pub remote_connection: Option, } #[derive(Debug)] @@ -51,6 +51,23 @@ pub enum OpenRequestKind { impl OpenRequest { pub fn parse(request: RawOpenRequest, cx: &App) -> Result { let mut this = Self::default(); + + this.diff_paths = request.diff_paths; + if let Some(wsl) = request.wsl { + let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { + if user.is_empty() { + anyhow::bail!("user is empty in wsl argument"); + } + (Some(user.to_string()), distro.to_string()) + } else { + (None, wsl) + }; + this.remote_connection = Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { + distro_name, + user, + })); + } + for url in request.urls { if let Some(server_name) = url.strip_prefix("zed-cli://") { this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?)); @@ -80,8 +97,6 @@ impl OpenRequest { } } - this.diff_paths = request.diff_paths; - Ok(this) } @@ -108,13 +123,15 @@ impl OpenRequest { if let Some(password) = url.password() { connection_options.password = Some(password.to_string()); } - if let Some(ssh_connection) = &self.ssh_connection { + + let connection_options = RemoteConnectionOptions::Ssh(connection_options); + if let Some(ssh_connection) = &self.remote_connection { anyhow::ensure!( *ssh_connection == connection_options, - "cannot open multiple ssh connections" + "cannot open multiple different remote connections" ); } - self.ssh_connection = Some(connection_options); + self.remote_connection = Some(connection_options); self.parse_file_path(url.path()); Ok(()) } @@ -152,6 +169,7 @@ pub struct OpenListener(UnboundedSender); pub struct RawOpenRequest { pub urls: Vec, pub diff_paths: Vec<[String; 2]>, + pub wsl: Option, } impl Global for OpenListener {} @@ -303,13 +321,21 @@ pub async fn handle_cli_connection( paths, diff_paths, wait, + wsl, open_new_workspace, env, user_data_dir: _, } => { if !urls.is_empty() { cx.update(|cx| { - match OpenRequest::parse(RawOpenRequest { urls, diff_paths }, cx) { + match OpenRequest::parse( + RawOpenRequest { + urls, + diff_paths, + wsl, + }, + cx, + ) { Ok(open_request) => { handle_open_request(open_request, app_state.clone(), cx); responses.send(CliResponse::Exit { status: 0 }).log_err(); @@ -422,30 +448,26 @@ async fn open_workspaces( errored = true } } - SerializedWorkspaceLocation::Ssh(ssh) => { + SerializedWorkspaceLocation::Remote(mut connection) => { let app_state = app_state.clone(); - let connection_options = cx.update(|cx| { - SshSettings::get_global(cx) - .connection_options_for(ssh.host, ssh.port, ssh.user) - }); - if let Ok(connection_options) = connection_options { - cx.spawn(async move |cx| { - open_ssh_project( - connection_options, - workspace_paths.paths().to_vec(), - app_state, - OpenOptions::default(), - cx, - ) - .await - .log_err(); - }) - .detach(); - // We don't set `errored` here if `open_ssh_project` fails, because for ssh projects, the - // error is displayed in the window. - } else { - errored = false; + if let RemoteConnectionOptions::Ssh(options) = &mut connection { + cx.update(|cx| { + SshSettings::get_global(cx) + .fill_connection_options_from_settings(options) + })?; } + cx.spawn(async move |cx| { + open_remote_project( + connection, + workspace_paths.paths().to_vec(), + app_state, + OpenOptions::default(), + cx, + ) + .await + .log_err(); + }) + .detach(); } } } @@ -587,6 +609,7 @@ mod tests { }; use editor::Editor; use gpui::TestAppContext; + use remote::SshConnectionOptions; use serde_json::json; use std::sync::Arc; use util::path; @@ -609,8 +632,8 @@ mod tests { .unwrap() }); assert_eq!( - request.ssh_connection.unwrap(), - SshConnectionOptions { + request.remote_connection.unwrap(), + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "localhost".into(), username: Some("me".into()), port: None, @@ -619,7 +642,7 @@ mod tests { port_forwards: None, nickname: None, upload_binary_over_ssh: false, - } + }) ); assert_eq!(request.open_paths, vec!["/"]); } diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index bd62dea75aac5ad2c4b01c4b17d8d6219b9110db..1dd51b5ffbd7c11cce0346142834581c022f512d 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -153,6 +153,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { urls, diff_paths, wait: false, + wsl: args.wsl.clone(), open_new_workspace: None, env: None, user_data_dir: args.user_data_dir.clone(), diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 8ae02124918a2f7f47a1c6204f5199f6eb4e6056..84ad39fb706f9d3e0e4af73a68b468e0bea33ee1 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -150,6 +150,7 @@ function CollectFiles { Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force + Move-Item -Path "$innoDir\zed-wsl" -Destination "$innoDir\bin\zed" -Force Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force }