Add initial support for WSL (#37035)

Closes #36188

## Todo

* [x] CLI
* [x] terminals
* [x] tasks

## For future PRs
* debugging
* UI for opening WSL projects
* fixing workspace state restoration

Release Notes:

- Windows alpha: Zed now supports editing folders in WSL.

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
This commit is contained in:
Max Brunsfeld
2025-08-29 17:18:52 -07:00
committed by GitHub
co-authored by Junkui Zhang
parent 1c2e2a00fe
commit f78f3e7729
27 changed files with 1701 additions and 786 deletions
+24 -2
View File
@@ -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") {
+1
View File
@@ -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>>,
+63 -11
View File
@@ -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 {
+5 -6
View File
@@ -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();
}
}
+5
View File
@@ -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.
+8 -2
View File
@@ -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,
+2 -2
View File
@@ -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())
+1 -1
View File
@@ -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
)
}
};
+17 -16
View File
@@ -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,
+61 -62
View File
@@ -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 {
+2 -1
View File
@@ -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;
+90 -27
View File
@@ -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()
}
+335
View File
@@ -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))
}
+11 -330
View File
@@ -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 {
+494
View File
@@ -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())
}
+8 -5
View File
@@ -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}")),
+310 -128
View File
@@ -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,
host: String,
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,
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()
+24 -8
View File
@@ -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)?;
+23 -36
View File
@@ -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)
+25
View File
@@ -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
+42 -33
View File
@@ -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}")
}
}
}
+2 -2
View File
@@ -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,
+57 -34
View File
@@ -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(),
+1
View File
@@ -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
}