Detailed changes
@@ -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") {
@@ -14,6 +14,7 @@ pub enum CliRequest {
paths: Vec<String>,
urls: Vec<String>,
diff_paths: Vec<[String; 2]>,
+ wsl: Option<String>,
wait: bool,
open_new_workspace: Option<bool>,
env: Option<HashMap<String, String>>,
@@ -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<String>,
+ /// 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<String>,
/// 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<String> {
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<String> {
+ 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::<HashMap<_, _>>())
+ #[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::<HashMap<_, _>>())
+ }
};
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 {
@@ -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<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
- pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
+ pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
@@ -1779,16 +1779,15 @@ impl ExtensionStore {
}
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
- 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();
}
}
@@ -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.
@@ -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,
@@ -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<SshConnectionOptions> {
+ pub fn remote_connection_options(&self, cx: &App) -> Option<RemoteConnectionOptions> {
self.remote_client
.as_ref()
.map(|remote| remote.read(cx).connection_options())
@@ -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,
@@ -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<Self>,
) {
@@ -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<Self>) -> 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
)
}
};
@@ -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({
@@ -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<u16>,
username: Option<String>,
) -> 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<SharedString>,
status_message: Option<SharedString>,
@@ -144,7 +148,7 @@ pub struct SshPrompt {
editor: Entity<Editor>,
}
-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<SshPrompt>,
+pub struct RemoteConnectionModal {
+ pub(crate) prompt: Entity<RemoteConnectionPrompt>,
paths: Vec<PathBuf>,
finished: bool,
}
-impl SshPrompt {
+impl RemoteConnectionPrompt {
pub(crate) fn new(
- connection_options: &SshConnectionOptions,
+ connection_string: String,
+ nickname: Option<String>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) -> 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<PathBuf>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) -> 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<DismissEvent> for SshConnectionModal {}
+impl EventEmitter<DismissEvent> 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<SshPrompt>,
+ ui: WeakEntity<RemoteConnectionPrompt>,
known_password: Option<String>,
}
-impl remote::RemoteClientDelegate for SshClientDelegate {
+impl remote::RemoteClientDelegate for RemoteClientDelegate {
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, 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::<SshConnectionModal>(cx).is_some()
-}
-
pub fn connect_over_ssh(
unique_identifier: ConnectionIdentifier,
connection_options: SshConnectionOptions,
- ui: Entity<SshPrompt>,
+ ui: Entity<RemoteConnectionPrompt>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Option<Entity<RemoteClient>>>> {
@@ -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<PathBuf>,
app_state: Arc<AppState>,
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::<SshConnectionModal>(cx)?
+ .active_modal::<RemoteConnectionModal>(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::<SshConnectionModal>(cx) {
+ if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(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,
@@ -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<Editor>,
address_error: Option<SharedString>,
- ssh_prompt: Option<Entity<SshPrompt>>,
+ ssh_prompt: Option<Entity<RemoteConnectionPrompt>>,
_creating: Option<Task<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::<SshConnectionModal>(cx)
+ .active_modal::<RemoteConnectionModal>(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::<SshConnectionModal>(cx) {
+ if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(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 {
@@ -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;
@@ -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<ChannelClient>,
unique_identifier: String,
- connection_options: SshConnectionOptions,
+ connection_options: RemoteConnectionOptions,
path_style: PathStyle,
state: Option<State>,
}
@@ -290,6 +295,22 @@ impl RemoteClient {
cancellation: oneshot::Receiver<()>,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut App,
+ ) -> Task<Result<Option<Entity<Self>>>> {
+ 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<dyn RemoteClientDelegate>,
+ cx: &mut App,
) -> Task<Result<Option<Entity<Self>>>> {
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::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
@@ -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<String>,
@@ -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::<ConnectionPool>().connections.len() as u16 + 1);
- let opts = SshConnectionOptions {
+ let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions {
host: "<fake>".to_string(),
port: Some(port),
..Default::default()
- };
+ });
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
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<Self> {
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<SshConnectionOptions, ConnectionPoolEntry>,
+ connections: HashMap<RemoteConnectionOptions, ConnectionPoolEntry>,
}
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<dyn RemoteClientDelegate>,
cx: &mut App,
) -> Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>> {
@@ -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<dyn RemoteConnection>);
+ let connection = match opts.clone() {
+ RemoteConnectionOptions::Ssh(opts) => {
+ SshRemoteConnection::new(opts, delegate, cx)
+ .await
+ .map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
+ }
+ RemoteConnectionOptions::Wsl(opts) => {
+ WslRemoteConnection::new(opts, delegate, cx)
+ .await
+ .map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
+ }
+ };
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<SshConnectionOptions> for RemoteConnectionOptions {
+ fn from(opts: SshConnectionOptions) -> Self {
+ RemoteConnectionOptions::Ssh(opts)
+ }
+}
+
+impl From<WslConnectionOptions> 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<Result<()>>;
async fn kill(&self) -> Result<()>;
fn has_been_killed(&self) -> bool;
+ fn shares_network_interface(&self) -> bool {
+ false
+ }
fn build_command(
&self,
program: Option<String>,
@@ -1000,7 +1063,7 @@ pub(crate) trait RemoteConnection: Send + Sync {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>;
- 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<ChannelClient>,
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()
}
@@ -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<Envelope>,
+ mut outgoing_rx: UnboundedReceiver<Envelope>,
+ mut connection_activity_tx: Sender<()>,
+ cx: &AsyncApp,
+) -> Task<Result<i32>> {
+ 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<anyhow::Result<()>> = 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::<LogRecord>(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<Option<std::path::PathBuf>> {
+ 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))
+}
@@ -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<Envelope>,
- mut outgoing_rx: UnboundedReceiver<Envelope>,
- mut connection_activity_tx: Sender<()>,
- cx: &AsyncApp,
- ) -> Task<Result<i32>> {
- 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<anyhow::Result<()>> = 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::<LogRecord>(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<dyn RemoteClientDelegate>,
@@ -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<dyn RemoteClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Result<PathBuf> {
- 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 {
@@ -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<String>,
+}
+
+pub(crate) struct WslRemoteConnection {
+ remote_binary_path: Option<RemotePathBuf>,
+ platform: RemotePlatform,
+ shell: String,
+ connection_options: WslConnectionOptions,
+}
+
+impl WslRemoteConnection {
+ pub(crate) async fn new(
+ connection_options: WslConnectionOptions,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Result<Self> {
+ 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<RemotePlatform> {
+ 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<String> {
+ 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<String> {
+ 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<String> {
+ run_wsl_command_impl(&self.connection_options, program, args).await
+ }
+
+ async fn ensure_server_binary(
+ &self,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ release_channel: ReleaseChannel,
+ version: SemanticVersion,
+ commit: Option<AppCommitSha>,
+ cx: &mut AsyncApp,
+ ) -> Result<RemotePathBuf> {
+ 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<dyn RemoteClientDelegate>,
+ 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<dyn RemoteClientDelegate>,
+ 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<Envelope>,
+ outgoing_rx: UnboundedReceiver<Envelope>,
+ connection_activity_tx: Sender<()>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<i32>> {
+ 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<Result<()>> {
+ 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<String>,
+ args: &[String],
+ env: &HashMap<String, String>,
+ working_dir: Option<String>,
+ port_forward: Option<(u16, String, u16)>,
+ ) -> Result<CommandTemplate> {
+ 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<String> {
+ 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<String> {
+ 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<String> {
+ 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())
+}
@@ -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<Self>) -> Option<AnyElement> {
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}")),
@@ -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<P: AsRef<Path>>(
+ pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
&self,
worktree_roots: &[P],
- ssh_project_id: SshConnectionId,
+ ssh_project_id: RemoteConnectionId,
) -> Option<SerializedWorkspace> {
self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id))
}
@@ -661,7 +745,7 @@ impl WorkspaceDb {
pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
&self,
worktree_roots: &[P],
- ssh_connection_id: Option<SshConnectionId>,
+ remote_connection_id: Option<RemoteConnectionId>,
) -> Option<SerializedWorkspace> {
// 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<u16>,
- user: Option<String>,
- ) -> Result<SshConnectionId> {
- self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user))
+ options: RemoteConnectionOptions,
+ ) -> Result<RemoteConnectionId> {
+ 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<RemoteConnectionId> {
+ 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<String>,
port: Option<u16>,
user: Option<String>,
- ) -> Result<SshConnectionId> {
+ distro: Option<String>,
+ ) -> Result<RemoteConnectionId> {
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<Vec<(WorkspaceId, PathList, Option<u64>)>> {
+ fn recent_workspaces(
+ &self,
+ ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
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<Vec<(PathList, Option<u64>, Option<SshConnectionId>)>> {
+ ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
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<Vec<(String, String, Option<u64>, Option<u64>)>> {
- 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<HashMap<SshConnectionId, SerializedSshConnection>> {
- 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<Vec<(u64, String, Option<u16>, Option<String>)>> {
- SELECT id, host, port, user
- FROM ssh_connections
- }
- }
-
- pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result<SerializedSshConnection> {
- let row = self.ssh_connection_query(id.0)?;
- Ok(SerializedSshConnection {
- host: row.0,
- port: row.1,
- user: row.2,
+ fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
+ 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<u16>, Option<String>)> {
- SELECT host, port, user
- FROM ssh_connections
+ pub(crate) fn remote_connection(
+ &self,
+ id: RemoteConnectionId,
+ ) -> Result<RemoteConnectionOptions> {
+ 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<String>,
+ port: Option<u16>,
+ user: Option<String>,
+ distro: Option<String>,
+ ) -> Option<RemoteConnectionOptions> {
+ 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<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
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<Vec<(SerializedWorkspaceLocation, PathList)>> {
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::<Vec<_>>();
- 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()
@@ -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<u16>,
- pub user: Option<String>,
+#[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<Self> {
+ 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)?;
@@ -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<Workspace>,
- connection_options: SshConnectionOptions,
+ connection_options: RemoteConnectionOptions,
cancel_rx: oneshot::Receiver<()>,
delegate: Arc<dyn RemoteClientDelegate>,
app_state: Arc<AppState>,
@@ -7293,11 +7286,11 @@ pub fn open_ssh_project_with_new_connection(
) -> Task<Result<()>> {
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<Project>,
paths: Vec<PathBuf>,
app_state: Arc<AppState>,
@@ -7346,9 +7339,9 @@ pub fn open_ssh_project_with_existing_connection(
) -> Task<Result<()>> {
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<Project>,
paths: Vec<PathBuf>,
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<PathBuf>,
cx: &AsyncApp,
) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
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<u16>,
- user: Option<String>,
+pub fn remote_workspace_position_from_db(
+ connection_options: RemoteConnectionOptions,
paths_to_open: &[PathBuf],
cx: &App,
) -> Task<Result<WorkspacePosition>> {
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)
@@ -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
@@ -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<AppState>, 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<PathBuf> = 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<AppState>, 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<String>,
+ /// 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<String>,
+
/// Instructs zed to run as a dev server on this machine. (not implemented)
#[arg(long)]
dev_server_token: Option<String>,
@@ -1242,18 +1251,18 @@ impl ToString for IdType {
}
}
-fn parse_url_arg(arg: &str, cx: &App) -> Result<String> {
+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}")
}
}
}
@@ -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,
@@ -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<String>)>,
pub join_channel: Option<u64>,
- pub ssh_connection: Option<SshConnectionOptions>,
+ pub remote_connection: Option<RemoteConnectionOptions>,
}
#[derive(Debug)]
@@ -51,6 +51,23 @@ pub enum OpenRequestKind {
impl OpenRequest {
pub fn parse(request: RawOpenRequest, cx: &App) -> Result<Self> {
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<RawOpenRequest>);
pub struct RawOpenRequest {
pub urls: Vec<String>,
pub diff_paths: Vec<[String; 2]>,
+ pub wsl: Option<String>,
}
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!["/"]);
}
@@ -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(),
@@ -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
}