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:
co-authored by
Junkui Zhang
parent
1c2e2a00fe
commit
f78f3e7729
@@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED;
|
||||
type Job = fn(&Path) -> Result<()>;
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) const JOBS: [Job; 6] = [
|
||||
pub(crate) const JOBS: &[Job] = &[
|
||||
// Delete old files
|
||||
|app_dir| {
|
||||
let zed_executable = app_dir.join("Zed.exe");
|
||||
@@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [
|
||||
std::fs::remove_file(&zed_cli)
|
||||
.context(format!("Failed to remove old file {}", zed_cli.display()))
|
||||
},
|
||||
|app_dir| {
|
||||
let zed_wsl = app_dir.join("bin\\zed");
|
||||
log::info!("Removing old file: {}", zed_wsl.display());
|
||||
std::fs::remove_file(&zed_wsl)
|
||||
.context(format!("Failed to remove old file {}", zed_wsl.display()))
|
||||
},
|
||||
// Copy new files
|
||||
|app_dir| {
|
||||
let zed_executable_source = app_dir.join("install\\Zed.exe");
|
||||
@@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [
|
||||
zed_cli_dest.display()
|
||||
))
|
||||
},
|
||||
|app_dir| {
|
||||
let zed_wsl_source = app_dir.join("install\\bin\\zed");
|
||||
let zed_wsl_dest = app_dir.join("bin\\zed");
|
||||
log::info!(
|
||||
"Copying new file {} to {}",
|
||||
zed_wsl_source.display(),
|
||||
zed_wsl_dest.display()
|
||||
);
|
||||
std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
|
||||
.map(|_| ())
|
||||
.context(format!(
|
||||
"Failed to copy new file {} to {}",
|
||||
zed_wsl_source.display(),
|
||||
zed_wsl_dest.display()
|
||||
))
|
||||
},
|
||||
// Clean up installer folder and updates folder
|
||||
|app_dir| {
|
||||
let updates_folder = app_dir.join("updates");
|
||||
@@ -85,7 +107,7 @@ pub(crate) const JOBS: [Job; 6] = [
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const JOBS: [Job; 2] = [
|
||||
pub(crate) const JOBS: &[Job] = &[
|
||||
|_| {
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
|
||||
|
||||
@@ -14,6 +14,7 @@ pub enum CliRequest {
|
||||
paths: Vec<String>,
|
||||
urls: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
wsl: Option<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
|
||||
+63
-11
@@ -6,7 +6,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
env, fs, io,
|
||||
@@ -85,6 +84,15 @@ struct Args {
|
||||
/// Run zed in dev-server mode
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
/// The username and WSL distribution to use when opening paths. ,If not specified,
|
||||
/// Zed will attempt to open the paths directly.
|
||||
///
|
||||
/// The username is optional, and if not specified, the default user for the distribution
|
||||
/// will be used.
|
||||
///
|
||||
/// Example: `me@Ubuntu` or `Ubuntu` for default distribution.
|
||||
#[arg(long, value_name = "USER@DISTRO")]
|
||||
wsl: Option<String>,
|
||||
/// Not supported in Zed CLI, only supported on Zed binary
|
||||
/// Will attempt to give the correct command to run
|
||||
#[arg(long)]
|
||||
@@ -129,14 +137,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||
unsafe {
|
||||
use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
||||
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
||||
let mut command = util::command::new_std_command("wsl.exe");
|
||||
|
||||
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||
if user.is_empty() {
|
||||
anyhow::bail!("user is empty in wsl argument");
|
||||
}
|
||||
(Some(user), distro)
|
||||
} else {
|
||||
(None, wsl)
|
||||
};
|
||||
|
||||
if let Some(user) = user {
|
||||
command.arg("--user").arg(user);
|
||||
}
|
||||
|
||||
let output = command
|
||||
.arg("--distribution")
|
||||
.arg(distro_name)
|
||||
.arg("wslpath")
|
||||
.arg("-m")
|
||||
.arg(source)
|
||||
.output()?;
|
||||
|
||||
let result = String::from_utf8_lossy(&output.stdout);
|
||||
let prefix = format!("//wsl.localhost/{}", distro_name);
|
||||
|
||||
Ok(result
|
||||
.trim()
|
||||
.strip_prefix(&prefix)
|
||||
.unwrap_or(&result)
|
||||
.to_string())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
util::prevent_root_execution();
|
||||
|
||||
@@ -223,6 +258,8 @@ fn main() -> Result<()> {
|
||||
let env = {
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
use collections::HashMap;
|
||||
|
||||
// On Linux, the desktop entry uses `cli` to spawn `zed`.
|
||||
// We need to handle env vars correctly since std::env::vars() may not contain
|
||||
// project-specific vars (e.g. those set by direnv).
|
||||
@@ -235,8 +272,19 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
Some(std::env::vars().collect::<HashMap<_, _>>())
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, by default, a child process inherits a copy of the environment block of the parent process.
|
||||
// So we don't need to pass env vars explicitly.
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
|
||||
{
|
||||
use collections::HashMap;
|
||||
|
||||
Some(std::env::vars().collect::<HashMap<_, _>>())
|
||||
}
|
||||
};
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
@@ -271,8 +319,10 @@ fn main() -> Result<()> {
|
||||
paths.push(tmp_file.path().to_string_lossy().to_string());
|
||||
let (tmp_file, _) = tmp_file.keep()?;
|
||||
anonymous_fd_tmp_files.push((file, tmp_file));
|
||||
} else if let Some(wsl) = &args.wsl {
|
||||
urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?)
|
||||
paths.push(parse_path_with_position(path)?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +342,7 @@ fn main() -> Result<()> {
|
||||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl: args.wsl,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
env,
|
||||
@@ -644,15 +695,15 @@ mod windows {
|
||||
Storage::FileSystem::{
|
||||
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
|
||||
},
|
||||
System::Threading::CreateMutexW,
|
||||
System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
|
||||
},
|
||||
core::HSTRING,
|
||||
};
|
||||
|
||||
use crate::{Detect, InstalledApp};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::{io, os::windows::process::CommandExt};
|
||||
|
||||
fn check_single_instance() -> bool {
|
||||
let mutex = unsafe {
|
||||
@@ -691,6 +742,7 @@ mod windows {
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
if check_single_instance() {
|
||||
std::process::Command::new(self.0.clone())
|
||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||
.arg(ipc_url)
|
||||
.spawn()?;
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,7 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::RemoteClient;
|
||||
use remote::{RemoteClient, RemoteConnectionOptions};
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -117,7 +117,7 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
|
||||
pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
@@ -1779,16 +1779,15 @@ impl ExtensionStore {
|
||||
}
|
||||
|
||||
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
let ssh_url = connection_options.ssh_url();
|
||||
let options = client.read(cx).connection_options();
|
||||
|
||||
if let Some(existing_client) = self.remote_clients.get(&ssh_url)
|
||||
if let Some(existing_client) = self.remote_clients.get(&options)
|
||||
&& existing_client.upgrade().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.remote_clients.insert(ssh_url, client.downgrade());
|
||||
self.remote_clients.insert(options, client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ pub fn remote_server_dir_relative() -> &'static Path {
|
||||
Path::new(".zed_server")
|
||||
}
|
||||
|
||||
/// Returns the relative path to the zed_wsl_server directory on the wsl host.
|
||||
pub fn remote_wsl_server_dir_relative() -> &'static Path {
|
||||
Path::new(".zed_wsl_server")
|
||||
}
|
||||
|
||||
/// Sets a custom directory for all user data, overriding the default data directory.
|
||||
/// This function must be called before any other path operations that depend on the data directory.
|
||||
/// The directory's path will be canonicalized to an absolute path by a blocking FS operation.
|
||||
|
||||
@@ -258,8 +258,14 @@ impl DapStore {
|
||||
let connection;
|
||||
if let Some(c) = binary.connection {
|
||||
let host = Ipv4Addr::LOCALHOST;
|
||||
let port = dap::transport::TcpTransport::unused_port(host).await?;
|
||||
port_forwarding = Some((port, c.host.to_string(), c.port));
|
||||
let port;
|
||||
if remote.read_with(cx, |remote, _cx| remote.shares_network_interface())? {
|
||||
port = c.port;
|
||||
port_forwarding = None;
|
||||
} else {
|
||||
port = dap::transport::TcpTransport::unused_port(host).await?;
|
||||
port_forwarding = Some((port, c.host.to_string(), c.port));
|
||||
}
|
||||
connection = Some(TcpArguments {
|
||||
port,
|
||||
host,
|
||||
|
||||
@@ -87,7 +87,7 @@ use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
pub use prettier_store::PrettierStore;
|
||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
use remote::{RemoteClient, SshConnectionOptions};
|
||||
use remote::{RemoteClient, RemoteConnectionOptions};
|
||||
use rpc::{
|
||||
AnyProtoClient, ErrorCode,
|
||||
proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
|
||||
@@ -1916,7 +1916,7 @@ impl Project {
|
||||
.map(|remote| remote.read(cx).connection_state())
|
||||
}
|
||||
|
||||
pub fn remote_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
|
||||
pub fn remote_connection_options(&self, cx: &App) -> Option<RemoteConnectionOptions> {
|
||||
self.remote_client
|
||||
.as_ref()
|
||||
.map(|remote| remote.read(cx).connection_options())
|
||||
|
||||
@@ -512,7 +512,7 @@ fn create_remote_shell(
|
||||
*env = command.env;
|
||||
|
||||
log::debug!("Connecting to a remote server: {:?}", command.program);
|
||||
let host = remote_client.read(cx).connection_options().host;
|
||||
let host = remote_client.read(cx).connection_options().display_name();
|
||||
|
||||
Ok(Shell::WithArguments {
|
||||
program: command.program,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use remote::SshConnectionOptions;
|
||||
use remote::RemoteConnectionOptions;
|
||||
use settings::Settings;
|
||||
use ui::{
|
||||
Button, ButtonCommon, ButtonStyle, Clickable, Context, ElevationIndex, FluentBuilder, Headline,
|
||||
@@ -9,11 +9,11 @@ use ui::{
|
||||
};
|
||||
use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr};
|
||||
|
||||
use crate::open_ssh_project;
|
||||
use crate::open_remote_project;
|
||||
|
||||
enum Host {
|
||||
RemoteProject,
|
||||
SshRemoteProject(SshConnectionOptions),
|
||||
CollabGuestProject,
|
||||
RemoteServerProject(RemoteConnectionOptions),
|
||||
}
|
||||
|
||||
pub struct DisconnectedOverlay {
|
||||
@@ -66,9 +66,9 @@ impl DisconnectedOverlay {
|
||||
|
||||
let remote_connection_options = project.read(cx).remote_connection_options(cx);
|
||||
let host = if let Some(ssh_connection_options) = remote_connection_options {
|
||||
Host::SshRemoteProject(ssh_connection_options)
|
||||
Host::RemoteServerProject(ssh_connection_options)
|
||||
} else {
|
||||
Host::RemoteProject
|
||||
Host::CollabGuestProject
|
||||
};
|
||||
|
||||
workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay {
|
||||
@@ -86,14 +86,14 @@ impl DisconnectedOverlay {
|
||||
self.finished = true;
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
if let Host::SshRemoteProject(ssh_connection_options) = &self.host {
|
||||
self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx);
|
||||
if let Host::RemoteServerProject(ssh_connection_options) = &self.host {
|
||||
self.reconnect_to_remote_project(ssh_connection_options.clone(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn reconnect_to_ssh_remote(
|
||||
fn reconnect_to_remote_project(
|
||||
&self,
|
||||
connection_options: SshConnectionOptions,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -114,7 +114,7 @@ impl DisconnectedOverlay {
|
||||
.collect();
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
open_ssh_project(
|
||||
open_remote_project(
|
||||
connection_options,
|
||||
paths,
|
||||
app_state,
|
||||
@@ -138,13 +138,13 @@ impl DisconnectedOverlay {
|
||||
|
||||
impl Render for DisconnectedOverlay {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let can_reconnect = matches!(self.host, Host::SshRemoteProject(_));
|
||||
let can_reconnect = matches!(self.host, Host::RemoteServerProject(_));
|
||||
|
||||
let message = match &self.host {
|
||||
Host::RemoteProject => {
|
||||
Host::CollabGuestProject => {
|
||||
"Your connection to the remote project has been lost.".to_string()
|
||||
}
|
||||
Host::SshRemoteProject(options) => {
|
||||
Host::RemoteServerProject(options) => {
|
||||
let autosave = if ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers
|
||||
@@ -155,7 +155,8 @@ impl Render for DisconnectedOverlay {
|
||||
};
|
||||
format!(
|
||||
"Your connection to {} has been lost.{}",
|
||||
options.host, autosave
|
||||
options.display_name(),
|
||||
autosave
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
pub mod disconnected_overlay;
|
||||
mod remote_connections;
|
||||
mod remote_servers;
|
||||
mod ssh_config;
|
||||
mod ssh_connections;
|
||||
|
||||
pub use ssh_connections::{is_connecting_over_ssh, open_ssh_project};
|
||||
use remote::RemoteConnectionOptions;
|
||||
pub use remote_connections::open_remote_project;
|
||||
|
||||
use disconnected_overlay::DisconnectedOverlay;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
@@ -16,9 +17,9 @@ use picker::{
|
||||
Picker, PickerDelegate,
|
||||
highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},
|
||||
};
|
||||
pub use remote_connections::SshSettings;
|
||||
pub use remote_servers::RemoteServerProjects;
|
||||
use settings::Settings;
|
||||
pub use ssh_connections::SshSettings;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
|
||||
use util::{ResultExt, paths::PathExt};
|
||||
@@ -290,7 +291,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
if workspace.database_id() == Some(*candidate_workspace_id) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
match candidate_workspace_location {
|
||||
match candidate_workspace_location.clone() {
|
||||
SerializedWorkspaceLocation::Local => {
|
||||
let paths = candidate_workspace_paths.paths().to_vec();
|
||||
if replace_current_window {
|
||||
@@ -320,7 +321,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
workspace.open_workspace_for_paths(false, paths, window, cx)
|
||||
}
|
||||
}
|
||||
SerializedWorkspaceLocation::Ssh(connection) => {
|
||||
SerializedWorkspaceLocation::Remote(mut connection) => {
|
||||
let app_state = workspace.app_state().clone();
|
||||
|
||||
let replace_window = if replace_current_window {
|
||||
@@ -334,18 +335,16 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let connection_options = SshSettings::get_global(cx)
|
||||
.connection_options_for(
|
||||
connection.host.clone(),
|
||||
connection.port,
|
||||
connection.user.clone(),
|
||||
);
|
||||
if let RemoteConnectionOptions::Ssh(connection) = &mut connection {
|
||||
SshSettings::get_global(cx)
|
||||
.fill_connection_options_from_settings(connection);
|
||||
};
|
||||
|
||||
let paths = candidate_workspace_paths.paths().to_vec();
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
open_ssh_project(
|
||||
connection_options,
|
||||
open_remote_project(
|
||||
connection.clone(),
|
||||
paths,
|
||||
app_state,
|
||||
open_options,
|
||||
@@ -418,9 +417,11 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
SerializedWorkspaceLocation::Remote(_) => {
|
||||
Icon::new(IconName::Server)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
})
|
||||
.child({
|
||||
|
||||
+73
-65
@@ -16,7 +16,8 @@ use language::CursorShape;
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{
|
||||
ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption,
|
||||
ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform,
|
||||
SshConnectionOptions, SshPortForwardOption,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -42,32 +43,35 @@ impl SshSettings {
|
||||
self.ssh_connections.clone().into_iter().flatten()
|
||||
}
|
||||
|
||||
pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
|
||||
for conn in self.ssh_connections() {
|
||||
if conn.host == options.host
|
||||
&& conn.username == options.username
|
||||
&& conn.port == options.port
|
||||
{
|
||||
options.nickname = conn.nickname;
|
||||
options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default();
|
||||
options.args = Some(conn.args);
|
||||
options.port_forwards = conn.port_forwards;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connection_options_for(
|
||||
&self,
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
username: Option<String>,
|
||||
) -> SshConnectionOptions {
|
||||
for conn in self.ssh_connections() {
|
||||
if conn.host == host && conn.username == username && conn.port == port {
|
||||
return SshConnectionOptions {
|
||||
nickname: conn.nickname,
|
||||
upload_binary_over_ssh: conn.upload_binary_over_ssh.unwrap_or_default(),
|
||||
args: Some(conn.args),
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
port_forwards: conn.port_forwards,
|
||||
password: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
SshConnectionOptions {
|
||||
let mut options = SshConnectionOptions {
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
self.fill_connection_options_from_settings(&mut options);
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +139,7 @@ impl Settings for SshSettings {
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
pub struct SshPrompt {
|
||||
pub struct RemoteConnectionPrompt {
|
||||
connection_string: SharedString,
|
||||
nickname: Option<SharedString>,
|
||||
status_message: Option<SharedString>,
|
||||
@@ -144,7 +148,7 @@ pub struct SshPrompt {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl Drop for SshPrompt {
|
||||
impl Drop for RemoteConnectionPrompt {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cancel) = self.cancellation.take() {
|
||||
cancel.send(()).ok();
|
||||
@@ -152,24 +156,22 @@ impl Drop for SshPrompt {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SshConnectionModal {
|
||||
pub(crate) prompt: Entity<SshPrompt>,
|
||||
pub struct RemoteConnectionModal {
|
||||
pub(crate) prompt: Entity<RemoteConnectionPrompt>,
|
||||
paths: Vec<PathBuf>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl SshPrompt {
|
||||
impl RemoteConnectionPrompt {
|
||||
pub(crate) fn new(
|
||||
connection_options: &SshConnectionOptions,
|
||||
connection_string: String,
|
||||
nickname: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let connection_string = connection_options.connection_string().into();
|
||||
let nickname = connection_options.nickname.clone().map(|s| s.into());
|
||||
|
||||
Self {
|
||||
connection_string,
|
||||
nickname,
|
||||
connection_string: connection_string.into(),
|
||||
nickname: nickname.map(|nickname| nickname.into()),
|
||||
editor: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
status_message: None,
|
||||
cancellation: None,
|
||||
@@ -232,7 +234,7 @@ impl SshPrompt {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SshPrompt {
|
||||
impl Render for RemoteConnectionPrompt {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
|
||||
@@ -297,15 +299,22 @@ impl Render for SshPrompt {
|
||||
}
|
||||
}
|
||||
|
||||
impl SshConnectionModal {
|
||||
impl RemoteConnectionModal {
|
||||
pub(crate) fn new(
|
||||
connection_options: &SshConnectionOptions,
|
||||
connection_options: &RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (connection_string, nickname) = match connection_options {
|
||||
RemoteConnectionOptions::Ssh(options) => {
|
||||
(options.connection_string(), options.nickname.clone())
|
||||
}
|
||||
RemoteConnectionOptions::Wsl(options) => (options.distro_name.clone(), None),
|
||||
};
|
||||
Self {
|
||||
prompt: cx.new(|cx| SshPrompt::new(connection_options, window, cx)),
|
||||
prompt: cx
|
||||
.new(|cx| RemoteConnectionPrompt::new(connection_string, nickname, window, cx)),
|
||||
finished: false,
|
||||
paths,
|
||||
}
|
||||
@@ -386,7 +395,7 @@ impl RenderOnce for SshConnectionHeader {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SshConnectionModal {
|
||||
impl Render for RemoteConnectionModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
let nickname = self.prompt.read(cx).nickname.clone();
|
||||
let connection_string = self.prompt.read(cx).connection_string.clone();
|
||||
@@ -423,15 +432,15 @@ impl Render for SshConnectionModal {
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for SshConnectionModal {
|
||||
impl Focusable for RemoteConnectionModal {
|
||||
fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
|
||||
self.prompt.read(cx).editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SshConnectionModal {}
|
||||
impl EventEmitter<DismissEvent> for RemoteConnectionModal {}
|
||||
|
||||
impl ModalView for SshConnectionModal {
|
||||
impl ModalView for RemoteConnectionModal {
|
||||
fn on_before_dismiss(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
@@ -446,13 +455,13 @@ impl ModalView for SshConnectionModal {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SshClientDelegate {
|
||||
pub struct RemoteClientDelegate {
|
||||
window: AnyWindowHandle,
|
||||
ui: WeakEntity<SshPrompt>,
|
||||
ui: WeakEntity<RemoteConnectionPrompt>,
|
||||
known_password: Option<String>,
|
||||
}
|
||||
|
||||
impl remote::RemoteClientDelegate for SshClientDelegate {
|
||||
impl remote::RemoteClientDelegate for RemoteClientDelegate {
|
||||
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
@@ -522,7 +531,7 @@ impl remote::RemoteClientDelegate for SshClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
impl SshClientDelegate {
|
||||
impl RemoteClientDelegate {
|
||||
fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||
self.window
|
||||
.update(cx, |_, _, cx| {
|
||||
@@ -534,14 +543,10 @@ impl SshClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &App) -> bool {
|
||||
workspace.active_modal::<SshConnectionModal>(cx).is_some()
|
||||
}
|
||||
|
||||
pub fn connect_over_ssh(
|
||||
unique_identifier: ConnectionIdentifier,
|
||||
connection_options: SshConnectionOptions,
|
||||
ui: Entity<SshPrompt>,
|
||||
ui: Entity<RemoteConnectionPrompt>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<RemoteClient>>>> {
|
||||
@@ -554,7 +559,7 @@ pub fn connect_over_ssh(
|
||||
unique_identifier,
|
||||
connection_options,
|
||||
rx,
|
||||
Arc::new(SshClientDelegate {
|
||||
Arc::new(RemoteClientDelegate {
|
||||
window,
|
||||
ui: ui.downgrade(),
|
||||
known_password,
|
||||
@@ -563,8 +568,8 @@ pub fn connect_over_ssh(
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn open_ssh_project(
|
||||
connection_options: SshConnectionOptions,
|
||||
pub async fn open_remote_project(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
open_options: workspace::OpenOptions,
|
||||
@@ -575,13 +580,7 @@ pub async fn open_ssh_project(
|
||||
} else {
|
||||
let workspace_position = cx
|
||||
.update(|cx| {
|
||||
workspace::ssh_workspace_position_from_db(
|
||||
connection_options.host.clone(),
|
||||
connection_options.port,
|
||||
connection_options.username.clone(),
|
||||
&paths,
|
||||
cx,
|
||||
)
|
||||
workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
|
||||
})?
|
||||
.await
|
||||
.context("fetching ssh workspace position from db")?;
|
||||
@@ -611,16 +610,16 @@ pub async fn open_ssh_project(
|
||||
loop {
|
||||
let (cancel_tx, cancel_rx) = oneshot::channel();
|
||||
let delegate = window.update(cx, {
|
||||
let connection_options = connection_options.clone();
|
||||
let paths = paths.clone();
|
||||
let connection_options = connection_options.clone();
|
||||
move |workspace, window, cx| {
|
||||
window.activate_window();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
SshConnectionModal::new(&connection_options, paths, window, cx)
|
||||
RemoteConnectionModal::new(&connection_options, paths, window, cx)
|
||||
});
|
||||
|
||||
let ui = workspace
|
||||
.active_modal::<SshConnectionModal>(cx)?
|
||||
.active_modal::<RemoteConnectionModal>(cx)?
|
||||
.read(cx)
|
||||
.prompt
|
||||
.clone();
|
||||
@@ -629,19 +628,25 @@ pub async fn open_ssh_project(
|
||||
ui.set_cancellation_tx(cancel_tx);
|
||||
});
|
||||
|
||||
Some(Arc::new(SshClientDelegate {
|
||||
Some(Arc::new(RemoteClientDelegate {
|
||||
window: window.window_handle(),
|
||||
ui: ui.downgrade(),
|
||||
known_password: connection_options.password.clone(),
|
||||
known_password: if let RemoteConnectionOptions::Ssh(options) =
|
||||
&connection_options
|
||||
{
|
||||
options.password.clone()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}))
|
||||
}
|
||||
})?;
|
||||
|
||||
let Some(delegate) = delegate else { break };
|
||||
|
||||
let did_open_ssh_project = cx
|
||||
let did_open_project = cx
|
||||
.update(|cx| {
|
||||
workspace::open_ssh_project_with_new_connection(
|
||||
workspace::open_remote_project_with_new_connection(
|
||||
window,
|
||||
connection_options.clone(),
|
||||
cancel_rx,
|
||||
@@ -655,19 +660,22 @@ pub async fn open_ssh_project(
|
||||
|
||||
window
|
||||
.update(cx, |workspace, _, cx| {
|
||||
if let Some(ui) = workspace.active_modal::<SshConnectionModal>(cx) {
|
||||
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
|
||||
ui.update(cx, |modal, cx| modal.finished(cx))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let Err(e) = did_open_ssh_project {
|
||||
if let Err(e) = did_open_project {
|
||||
log::error!("Failed to open project: {e:?}");
|
||||
let response = window
|
||||
.update(cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Critical,
|
||||
"Failed to connect over SSH",
|
||||
match connection_options {
|
||||
RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
|
||||
RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
|
||||
},
|
||||
Some(&e.to_string()),
|
||||
&["Retry", "Ok"],
|
||||
cx,
|
||||
@@ -1,70 +1,52 @@
|
||||
use std::any::Any;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
|
||||
use crate::{
|
||||
remote_connections::{
|
||||
RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection,
|
||||
SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project,
|
||||
},
|
||||
ssh_config::parse_ssh_config_hosts,
|
||||
};
|
||||
use editor::Editor;
|
||||
use file_finder::OpenPathDelegate;
|
||||
use futures::FutureExt;
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::Shared;
|
||||
use futures::select;
|
||||
use gpui::ClickEvent;
|
||||
use gpui::ClipboardItem;
|
||||
use gpui::Subscription;
|
||||
use gpui::Task;
|
||||
use gpui::WeakEntity;
|
||||
use gpui::canvas;
|
||||
use futures::{FutureExt, channel::oneshot, future::Shared, select};
|
||||
use gpui::{
|
||||
AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
PromptLevel, ScrollHandle, Window,
|
||||
AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window,
|
||||
canvas,
|
||||
};
|
||||
use paths::global_ssh_config_file;
|
||||
use paths::user_ssh_config_file;
|
||||
use paths::{global_ssh_config_file, user_ssh_config_file};
|
||||
use picker::Picker;
|
||||
use project::Fs;
|
||||
use project::Project;
|
||||
use remote::remote_client::ConnectionIdentifier;
|
||||
use remote::{RemoteClient, SshConnectionOptions};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use settings::update_settings_file;
|
||||
use settings::watch_config_file;
|
||||
use project::{Fs, Project};
|
||||
use remote::{
|
||||
RemoteClient, RemoteConnectionOptions, SshConnectionOptions,
|
||||
remote_client::ConnectionIdentifier,
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file, watch_config_file};
|
||||
use smol::stream::StreamExt as _;
|
||||
use ui::Navigable;
|
||||
use ui::NavigableEntry;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
collections::BTreeSet,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{self, AtomicUsize},
|
||||
},
|
||||
};
|
||||
use ui::{
|
||||
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState,
|
||||
Section, Tooltip, prelude::*,
|
||||
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Navigable, NavigableEntry,
|
||||
Scrollbar, ScrollbarState, Section, Tooltip, prelude::*,
|
||||
};
|
||||
use util::{
|
||||
ResultExt,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
};
|
||||
use workspace::OpenOptions;
|
||||
use workspace::Toast;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
ModalView, Workspace, notifications::DetachAndPromptErr,
|
||||
open_ssh_project_with_existing_connection,
|
||||
ModalView, OpenOptions, Toast, Workspace,
|
||||
notifications::{DetachAndPromptErr, NotificationId},
|
||||
open_remote_project_with_existing_connection,
|
||||
};
|
||||
|
||||
use crate::ssh_config::parse_ssh_config_hosts;
|
||||
use crate::ssh_connections::RemoteSettingsContent;
|
||||
use crate::ssh_connections::SshConnection;
|
||||
use crate::ssh_connections::SshConnectionHeader;
|
||||
use crate::ssh_connections::SshConnectionModal;
|
||||
use crate::ssh_connections::SshProject;
|
||||
use crate::ssh_connections::SshPrompt;
|
||||
use crate::ssh_connections::SshSettings;
|
||||
use crate::ssh_connections::connect_over_ssh;
|
||||
use crate::ssh_connections::open_ssh_project;
|
||||
|
||||
mod navigation_base {}
|
||||
pub struct RemoteServerProjects {
|
||||
mode: Mode,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -79,7 +61,7 @@ pub struct RemoteServerProjects {
|
||||
struct CreateRemoteServer {
|
||||
address_editor: Entity<Editor>,
|
||||
address_error: Option<SharedString>,
|
||||
ssh_prompt: Option<Entity<SshPrompt>>,
|
||||
ssh_prompt: Option<Entity<RemoteConnectionPrompt>>,
|
||||
_creating: Option<Task<Option<()>>>,
|
||||
}
|
||||
|
||||
@@ -222,8 +204,13 @@ impl ProjectPicker {
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
open_ssh_project_with_existing_connection(
|
||||
connection, project, paths, app_state, window, cx,
|
||||
open_remote_project_with_existing_connection(
|
||||
RemoteConnectionOptions::Ssh(connection),
|
||||
project,
|
||||
paths,
|
||||
app_state,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
@@ -472,7 +459,14 @@ impl RemoteServerProjects {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx));
|
||||
let ssh_prompt = cx.new(|cx| {
|
||||
RemoteConnectionPrompt::new(
|
||||
connection_options.connection_string(),
|
||||
connection_options.nickname.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let connection = connect_over_ssh(
|
||||
ConnectionIdentifier::setup(),
|
||||
@@ -552,15 +546,20 @@ impl RemoteServerProjects {
|
||||
};
|
||||
|
||||
let create_new_window = self.create_new_window;
|
||||
let connection_options = ssh_connection.into();
|
||||
let connection_options: SshConnectionOptions = ssh_connection.into();
|
||||
workspace.update(cx, |_, cx| {
|
||||
cx.defer_in(window, move |workspace, window, cx| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
SshConnectionModal::new(&connection_options, Vec::new(), window, cx)
|
||||
RemoteConnectionModal::new(
|
||||
&RemoteConnectionOptions::Ssh(connection_options.clone()),
|
||||
Vec::new(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let prompt = workspace
|
||||
.active_modal::<SshConnectionModal>(cx)
|
||||
.active_modal::<RemoteConnectionModal>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.prompt
|
||||
@@ -579,7 +578,7 @@ impl RemoteServerProjects {
|
||||
let session = connect.await;
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) {
|
||||
if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
|
||||
prompt.update(cx, |prompt, cx| prompt.finished(cx))
|
||||
}
|
||||
})?;
|
||||
@@ -898,8 +897,8 @@ impl RemoteServerProjects {
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let result = open_ssh_project(
|
||||
server.into(),
|
||||
let result = open_remote_project(
|
||||
RemoteConnectionOptions::Ssh(server.into()),
|
||||
project.paths.into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
OpenOptions {
|
||||
|
||||
@@ -6,6 +6,7 @@ mod transport;
|
||||
|
||||
pub use remote_client::{
|
||||
ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
|
||||
RemotePlatform,
|
||||
RemoteConnectionOptions, RemotePlatform,
|
||||
};
|
||||
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
|
||||
pub use transport::wsl::WslConnectionOptions;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use crate::{
|
||||
SshConnectionOptions, protocol::MessageId, proxy::ProxyLaunchError,
|
||||
transport::ssh::SshRemoteConnection,
|
||||
SshConnectionOptions,
|
||||
protocol::MessageId,
|
||||
proxy::ProxyLaunchError,
|
||||
transport::{
|
||||
ssh::SshRemoteConnection,
|
||||
wsl::{WslConnectionOptions, WslRemoteConnection},
|
||||
},
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
@@ -237,7 +242,7 @@ impl From<&State> for ConnectionState {
|
||||
pub struct RemoteClient {
|
||||
client: Arc<ChannelClient>,
|
||||
unique_identifier: String,
|
||||
connection_options: SshConnectionOptions,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
path_style: PathStyle,
|
||||
state: Option<State>,
|
||||
}
|
||||
@@ -290,6 +295,22 @@ impl RemoteClient {
|
||||
cancellation: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<Self>>>> {
|
||||
Self::new(
|
||||
unique_identifier,
|
||||
RemoteConnectionOptions::Ssh(connection_options),
|
||||
cancellation,
|
||||
delegate,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
unique_identifier: ConnectionIdentifier,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
cancellation: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<Self>>>> {
|
||||
let unique_identifier = unique_identifier.to_string(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
@@ -424,7 +445,7 @@ impl RemoteClient {
|
||||
}
|
||||
|
||||
let state = self.state.take().unwrap();
|
||||
let (attempts, ssh_connection, delegate) = match state {
|
||||
let (attempts, remote_connection, delegate) = match state {
|
||||
State::Connected {
|
||||
ssh_connection,
|
||||
delegate,
|
||||
@@ -482,15 +503,15 @@ impl RemoteClient {
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(error) = ssh_connection
|
||||
if let Err(error) = remote_connection
|
||||
.kill()
|
||||
.await
|
||||
.context("Failed to kill ssh process")
|
||||
{
|
||||
failed!(error, attempts, ssh_connection, delegate);
|
||||
failed!(error, attempts, remote_connection, delegate);
|
||||
};
|
||||
|
||||
let connection_options = ssh_connection.connection_options();
|
||||
let connection_options = remote_connection.connection_options();
|
||||
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
@@ -519,7 +540,7 @@ impl RemoteClient {
|
||||
{
|
||||
Ok((ssh_connection, io_task)) => (ssh_connection, io_task),
|
||||
Err(error) => {
|
||||
failed!(error, attempts, ssh_connection, delegate);
|
||||
failed!(error, attempts, remote_connection, delegate);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -751,6 +772,13 @@ impl RemoteClient {
|
||||
Some(self.state.as_ref()?.remote_connection()?.shell())
|
||||
}
|
||||
|
||||
pub fn shares_network_interface(&self) -> bool {
|
||||
self.state
|
||||
.as_ref()
|
||||
.and_then(|state| state.remote_connection())
|
||||
.map_or(false, |connection| connection.shares_network_interface())
|
||||
}
|
||||
|
||||
pub fn build_command(
|
||||
&self,
|
||||
program: Option<String>,
|
||||
@@ -789,11 +817,7 @@ impl RemoteClient {
|
||||
self.client.clone().into()
|
||||
}
|
||||
|
||||
pub fn host(&self) -> String {
|
||||
self.connection_options.host.clone()
|
||||
}
|
||||
|
||||
pub fn connection_options(&self) -> SshConnectionOptions {
|
||||
pub fn connection_options(&self) -> RemoteConnectionOptions {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
|
||||
@@ -836,14 +860,14 @@ impl RemoteClient {
|
||||
pub fn fake_server(
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
server_cx: &mut gpui::TestAppContext,
|
||||
) -> (SshConnectionOptions, AnyProtoClient) {
|
||||
) -> (RemoteConnectionOptions, AnyProtoClient) {
|
||||
let port = client_cx
|
||||
.update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
|
||||
let opts = SshConnectionOptions {
|
||||
let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "<fake>".to_string(),
|
||||
port: Some(port),
|
||||
..Default::default()
|
||||
};
|
||||
});
|
||||
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
|
||||
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
let server_client =
|
||||
@@ -874,13 +898,13 @@ impl RemoteClient {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn fake_client(
|
||||
opts: SshConnectionOptions,
|
||||
opts: RemoteConnectionOptions,
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
) -> Entity<Self> {
|
||||
let (_tx, rx) = oneshot::channel();
|
||||
client_cx
|
||||
.update(|cx| {
|
||||
Self::ssh(
|
||||
Self::new(
|
||||
ConnectionIdentifier::setup(),
|
||||
opts,
|
||||
rx,
|
||||
@@ -901,7 +925,7 @@ enum ConnectionPoolEntry {
|
||||
|
||||
#[derive(Default)]
|
||||
struct ConnectionPool {
|
||||
connections: HashMap<SshConnectionOptions, ConnectionPoolEntry>,
|
||||
connections: HashMap<RemoteConnectionOptions, ConnectionPoolEntry>,
|
||||
}
|
||||
|
||||
impl Global for ConnectionPool {}
|
||||
@@ -909,7 +933,7 @@ impl Global for ConnectionPool {}
|
||||
impl ConnectionPool {
|
||||
pub fn connect(
|
||||
&mut self,
|
||||
opts: SshConnectionOptions,
|
||||
opts: RemoteConnectionOptions,
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut App,
|
||||
) -> Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>> {
|
||||
@@ -939,9 +963,18 @@ impl ConnectionPool {
|
||||
let opts = opts.clone();
|
||||
let delegate = delegate.clone();
|
||||
async move |cx| {
|
||||
let connection = SshRemoteConnection::new(opts.clone(), delegate, cx)
|
||||
.await
|
||||
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>);
|
||||
let connection = match opts.clone() {
|
||||
RemoteConnectionOptions::Ssh(opts) => {
|
||||
SshRemoteConnection::new(opts, delegate, cx)
|
||||
.await
|
||||
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
|
||||
}
|
||||
RemoteConnectionOptions::Wsl(opts) => {
|
||||
WslRemoteConnection::new(opts, delegate, cx)
|
||||
.await
|
||||
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
|
||||
}
|
||||
};
|
||||
|
||||
cx.update_global(|pool: &mut Self, _| {
|
||||
debug_assert!(matches!(
|
||||
@@ -972,6 +1005,33 @@ impl ConnectionPool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RemoteConnectionOptions {
|
||||
Ssh(SshConnectionOptions),
|
||||
Wsl(WslConnectionOptions),
|
||||
}
|
||||
|
||||
impl RemoteConnectionOptions {
|
||||
pub fn display_name(&self) -> String {
|
||||
match self {
|
||||
RemoteConnectionOptions::Ssh(opts) => opts.host.clone(),
|
||||
RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshConnectionOptions> for RemoteConnectionOptions {
|
||||
fn from(opts: SshConnectionOptions) -> Self {
|
||||
RemoteConnectionOptions::Ssh(opts)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WslConnectionOptions> for RemoteConnectionOptions {
|
||||
fn from(opts: WslConnectionOptions) -> Self {
|
||||
RemoteConnectionOptions::Wsl(opts)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub(crate) trait RemoteConnection: Send + Sync {
|
||||
fn start_proxy(
|
||||
@@ -992,6 +1052,9 @@ pub(crate) trait RemoteConnection: Send + Sync {
|
||||
) -> Task<Result<()>>;
|
||||
async fn kill(&self) -> Result<()>;
|
||||
fn has_been_killed(&self) -> bool;
|
||||
fn shares_network_interface(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn build_command(
|
||||
&self,
|
||||
program: Option<String>,
|
||||
@@ -1000,7 +1063,7 @@ pub(crate) trait RemoteConnection: Send + Sync {
|
||||
working_dir: Option<String>,
|
||||
port_forward: Option<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate>;
|
||||
fn connection_options(&self) -> SshConnectionOptions;
|
||||
fn connection_options(&self) -> RemoteConnectionOptions;
|
||||
fn path_style(&self) -> PathStyle;
|
||||
fn shell(&self) -> String;
|
||||
|
||||
@@ -1307,7 +1370,7 @@ impl ProtoClient for ChannelClient {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod fake {
|
||||
use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform};
|
||||
use crate::{SshConnectionOptions, remote_client::CommandTemplate};
|
||||
use crate::remote_client::{CommandTemplate, RemoteConnectionOptions};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
@@ -1326,7 +1389,7 @@ mod fake {
|
||||
use util::paths::{PathStyle, RemotePathBuf};
|
||||
|
||||
pub(super) struct FakeRemoteConnection {
|
||||
pub(super) connection_options: SshConnectionOptions,
|
||||
pub(super) connection_options: RemoteConnectionOptions,
|
||||
pub(super) server_channel: Arc<ChannelClient>,
|
||||
pub(super) server_cx: SendableCx,
|
||||
}
|
||||
@@ -1386,7 +1449,7 @@ mod fake {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> SshConnectionOptions {
|
||||
fn connection_options(&self) -> RemoteConnectionOptions {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,336 @@
|
||||
use crate::{
|
||||
json_log::LogRecord,
|
||||
protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::{
|
||||
AsyncReadExt as _, FutureExt as _, StreamExt as _,
|
||||
channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender},
|
||||
};
|
||||
use gpui::{AppContext as _, AsyncApp, Task};
|
||||
use rpc::proto::Envelope;
|
||||
use smol::process::Child;
|
||||
|
||||
pub mod ssh;
|
||||
pub mod wsl;
|
||||
|
||||
fn handle_rpc_messages_over_child_process_stdio(
|
||||
mut ssh_proxy_process: Child,
|
||||
incoming_tx: UnboundedSender<Envelope>,
|
||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
mut connection_activity_tx: Sender<()>,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<i32>> {
|
||||
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
|
||||
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
|
||||
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
|
||||
|
||||
let mut stdin_buffer = Vec::new();
|
||||
let mut stdout_buffer = Vec::new();
|
||||
let mut stderr_buffer = Vec::new();
|
||||
let mut stderr_offset = 0;
|
||||
|
||||
let stdin_task = cx.background_spawn(async move {
|
||||
while let Some(outgoing) = outgoing_rx.next().await {
|
||||
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let stdout_task = cx.background_spawn({
|
||||
let mut connection_activity_tx = connection_activity_tx.clone();
|
||||
async move {
|
||||
loop {
|
||||
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||
let len = child_stdout.read(&mut stdout_buffer).await?;
|
||||
|
||||
if len == 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
if len < MESSAGE_LEN_SIZE {
|
||||
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
|
||||
}
|
||||
|
||||
let message_len = message_len_from_buffer(&stdout_buffer);
|
||||
let envelope =
|
||||
read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
|
||||
.await?;
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
incoming_tx.unbounded_send(envelope).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stderr_task: Task<anyhow::Result<()>> = cx.background_spawn(async move {
|
||||
loop {
|
||||
stderr_buffer.resize(stderr_offset + 1024, 0);
|
||||
|
||||
let len = child_stderr
|
||||
.read(&mut stderr_buffer[stderr_offset..])
|
||||
.await?;
|
||||
if len == 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
stderr_offset += len;
|
||||
let mut start_ix = 0;
|
||||
while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
|
||||
.iter()
|
||||
.position(|b| b == &b'\n')
|
||||
{
|
||||
let line_ix = start_ix + ix;
|
||||
let content = &stderr_buffer[start_ix..line_ix];
|
||||
start_ix = line_ix + 1;
|
||||
if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
|
||||
record.log(log::logger())
|
||||
} else {
|
||||
eprintln!("(remote) {}", String::from_utf8_lossy(content));
|
||||
}
|
||||
}
|
||||
stderr_buffer.drain(0..start_ix);
|
||||
stderr_offset -= start_ix;
|
||||
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
}
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let result = futures::select! {
|
||||
result = stdin_task.fuse() => {
|
||||
result.context("stdin")
|
||||
}
|
||||
result = stdout_task.fuse() => {
|
||||
result.context("stdout")
|
||||
}
|
||||
result = stderr_task.fuse() => {
|
||||
result.context("stderr")
|
||||
}
|
||||
};
|
||||
|
||||
let status = ssh_proxy_process.status().await?.code().unwrap_or(1);
|
||||
match result {
|
||||
Ok(_) => Ok(status),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn build_remote_server_from_source(
|
||||
platform: &crate::RemotePlatform,
|
||||
delegate: &dyn crate::RemoteClientDelegate,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Option<std::path::PathBuf>> {
|
||||
use std::path::Path;
|
||||
|
||||
let Some(build_remote_server) = std::env::var("ZED_BUILD_REMOTE_SERVER").ok() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
use smol::process::{Command, Stdio};
|
||||
use std::env::VarError;
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Failed to run command: {command:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let use_musl = !build_remote_server.contains("nomusl");
|
||||
let triple = format!(
|
||||
"{}-{}",
|
||||
platform.arch,
|
||||
match platform.os {
|
||||
"linux" =>
|
||||
if use_musl {
|
||||
"unknown-linux-musl"
|
||||
} else {
|
||||
"unknown-linux-gnu"
|
||||
},
|
||||
"macos" => "apple-darwin",
|
||||
_ => anyhow::bail!("can't cross compile for: {:?}", platform),
|
||||
}
|
||||
);
|
||||
let mut rust_flags = match std::env::var("RUSTFLAGS") {
|
||||
Ok(val) => val,
|
||||
Err(VarError::NotPresent) => String::new(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to get env var `RUSTFLAGS` value: {e}");
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
if platform.os == "linux" && use_musl {
|
||||
rust_flags.push_str(" -C target-feature=+crt-static");
|
||||
}
|
||||
if build_remote_server.contains("mold") {
|
||||
rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
|
||||
}
|
||||
|
||||
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
|
||||
delegate.set_status(Some("Building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(
|
||||
Command::new("cargo")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env("RUSTFLAGS", &rust_flags),
|
||||
)
|
||||
.await?;
|
||||
} else if build_remote_server.contains("cross") {
|
||||
#[cfg(target_os = "windows")]
|
||||
use util::paths::SanitizedPath;
|
||||
|
||||
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||
log::info!("installing cross");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"install",
|
||||
"cross",
|
||||
"--git",
|
||||
"https://github.com/cross-rs/cross",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
delegate.set_status(
|
||||
Some(&format!(
|
||||
"Building remote server binary from source for {} with Docker",
|
||||
&triple
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote server binary from source for {}", &triple);
|
||||
|
||||
// On Windows, the binding needs to be set to the canonical path
|
||||
#[cfg(target_os = "windows")]
|
||||
let src = SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let src = "./target";
|
||||
|
||||
run_cmd(
|
||||
Command::new("cross")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env(
|
||||
"CROSS_CONTAINER_OPTS",
|
||||
format!("--mount type=bind,src={src},dst=/app/target"),
|
||||
)
|
||||
.env("RUSTFLAGS", &rust_flags),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let which = cx
|
||||
.background_spawn(async move { which::which("zig") })
|
||||
.await;
|
||||
|
||||
if which.is_err() {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
|
||||
log::info!("adding rustup target");
|
||||
run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
|
||||
|
||||
delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
|
||||
log::info!("installing cargo-zigbuild");
|
||||
run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
|
||||
|
||||
delegate.set_status(
|
||||
Some(&format!(
|
||||
"Building remote binary from source for {triple} with Zig"
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote binary from source for {triple} with Zig");
|
||||
run_cmd(
|
||||
Command::new("cargo")
|
||||
.args([
|
||||
"zigbuild",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env("RUSTFLAGS", &rust_flags),
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
let bin_path = Path::new("target")
|
||||
.join("remote_server")
|
||||
.join(&triple)
|
||||
.join("debug")
|
||||
.join("remote_server");
|
||||
|
||||
let path = if !build_remote_server.contains("nocompress") {
|
||||
delegate.set_status(Some("Compressing binary"), cx);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, we use 7z to compress the binary
|
||||
let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
|
||||
let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
|
||||
if smol::fs::metadata(&gz_path).await.is_ok() {
|
||||
smol::fs::remove_file(&gz_path).await?;
|
||||
}
|
||||
run_cmd(Command::new(seven_zip).args([
|
||||
"a",
|
||||
"-tgzip",
|
||||
&gz_path,
|
||||
&bin_path.to_string_lossy(),
|
||||
]))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut archive_path = bin_path;
|
||||
archive_path.set_extension("gz");
|
||||
std::env::current_dir()?.join(archive_path)
|
||||
} else {
|
||||
bin_path
|
||||
};
|
||||
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use crate::{
|
||||
RemoteClientDelegate, RemotePlatform,
|
||||
json_log::LogRecord,
|
||||
protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
|
||||
remote_client::{CommandTemplate, RemoteConnection},
|
||||
remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
AsyncReadExt as _, FutureExt as _, StreamExt as _,
|
||||
AsyncReadExt as _, FutureExt as _,
|
||||
channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender},
|
||||
select_biased,
|
||||
};
|
||||
@@ -99,8 +97,8 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
self.master_process.lock().is_none()
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> SshConnectionOptions {
|
||||
self.socket.connection_options.clone()
|
||||
fn connection_options(&self) -> RemoteConnectionOptions {
|
||||
RemoteConnectionOptions::Ssh(self.socket.connection_options.clone())
|
||||
}
|
||||
|
||||
fn shell(&self) -> String {
|
||||
@@ -267,7 +265,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
}
|
||||
};
|
||||
|
||||
Self::multiplex(
|
||||
super::handle_rpc_messages_over_child_process_stdio(
|
||||
ssh_proxy_process,
|
||||
incoming_tx,
|
||||
outgoing_rx,
|
||||
@@ -415,109 +413,6 @@ impl SshRemoteConnection {
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn multiplex(
|
||||
mut ssh_proxy_process: Child,
|
||||
incoming_tx: UnboundedSender<Envelope>,
|
||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
mut connection_activity_tx: Sender<()>,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<i32>> {
|
||||
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
|
||||
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
|
||||
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
|
||||
|
||||
let mut stdin_buffer = Vec::new();
|
||||
let mut stdout_buffer = Vec::new();
|
||||
let mut stderr_buffer = Vec::new();
|
||||
let mut stderr_offset = 0;
|
||||
|
||||
let stdin_task = cx.background_spawn(async move {
|
||||
while let Some(outgoing) = outgoing_rx.next().await {
|
||||
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let stdout_task = cx.background_spawn({
|
||||
let mut connection_activity_tx = connection_activity_tx.clone();
|
||||
async move {
|
||||
loop {
|
||||
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||
let len = child_stdout.read(&mut stdout_buffer).await?;
|
||||
|
||||
if len == 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
if len < MESSAGE_LEN_SIZE {
|
||||
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
|
||||
}
|
||||
|
||||
let message_len = message_len_from_buffer(&stdout_buffer);
|
||||
let envelope =
|
||||
read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
|
||||
.await?;
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
incoming_tx.unbounded_send(envelope).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stderr_task: Task<anyhow::Result<()>> = cx.background_spawn(async move {
|
||||
loop {
|
||||
stderr_buffer.resize(stderr_offset + 1024, 0);
|
||||
|
||||
let len = child_stderr
|
||||
.read(&mut stderr_buffer[stderr_offset..])
|
||||
.await?;
|
||||
if len == 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
stderr_offset += len;
|
||||
let mut start_ix = 0;
|
||||
while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
|
||||
.iter()
|
||||
.position(|b| b == &b'\n')
|
||||
{
|
||||
let line_ix = start_ix + ix;
|
||||
let content = &stderr_buffer[start_ix..line_ix];
|
||||
start_ix = line_ix + 1;
|
||||
if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
|
||||
record.log(log::logger())
|
||||
} else {
|
||||
eprintln!("(remote) {}", String::from_utf8_lossy(content));
|
||||
}
|
||||
}
|
||||
stderr_buffer.drain(0..start_ix);
|
||||
stderr_offset -= start_ix;
|
||||
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
}
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let result = futures::select! {
|
||||
result = stdin_task.fuse() => {
|
||||
result.context("stdin")
|
||||
}
|
||||
result = stdout_task.fuse() => {
|
||||
result.context("stdout")
|
||||
}
|
||||
result = stderr_task.fuse() => {
|
||||
result.context("stderr")
|
||||
}
|
||||
};
|
||||
|
||||
let status = ssh_proxy_process.status().await?.code().unwrap_or(1);
|
||||
match result {
|
||||
Ok(_) => Ok(status),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
async fn ensure_server_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
@@ -544,19 +439,20 @@ impl SshRemoteConnection {
|
||||
self.ssh_path_style,
|
||||
);
|
||||
|
||||
let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(build_remote_server) = build_remote_server {
|
||||
let src_path = self.build_local(build_remote_server, delegate, cx).await?;
|
||||
if let Some(remote_server_path) =
|
||||
super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx)
|
||||
.await?
|
||||
{
|
||||
let tmp_path = RemotePathBuf::new(
|
||||
paths::remote_server_dir_relative().join(format!(
|
||||
"download-{}-{}",
|
||||
std::process::id(),
|
||||
src_path.file_name().unwrap().to_string_lossy()
|
||||
remote_server_path.file_name().unwrap().to_string_lossy()
|
||||
)),
|
||||
self.ssh_path_style,
|
||||
);
|
||||
self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
|
||||
self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx)
|
||||
.await?;
|
||||
self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
|
||||
.await?;
|
||||
@@ -794,221 +690,6 @@ impl SshRemoteConnection {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn build_local(
|
||||
&self,
|
||||
build_remote_server: String,
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<PathBuf> {
|
||||
use smol::process::{Command, Stdio};
|
||||
use std::env::VarError;
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Failed to run command: {command:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let use_musl = !build_remote_server.contains("nomusl");
|
||||
let triple = format!(
|
||||
"{}-{}",
|
||||
self.ssh_platform.arch,
|
||||
match self.ssh_platform.os {
|
||||
"linux" =>
|
||||
if use_musl {
|
||||
"unknown-linux-musl"
|
||||
} else {
|
||||
"unknown-linux-gnu"
|
||||
},
|
||||
"macos" => "apple-darwin",
|
||||
_ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform),
|
||||
}
|
||||
);
|
||||
let mut rust_flags = match std::env::var("RUSTFLAGS") {
|
||||
Ok(val) => val,
|
||||
Err(VarError::NotPresent) => String::new(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to get env var `RUSTFLAGS` value: {e}");
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
if self.ssh_platform.os == "linux" && use_musl {
|
||||
rust_flags.push_str(" -C target-feature=+crt-static");
|
||||
}
|
||||
if build_remote_server.contains("mold") {
|
||||
rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
|
||||
}
|
||||
|
||||
if self.ssh_platform.arch == std::env::consts::ARCH
|
||||
&& self.ssh_platform.os == std::env::consts::OS
|
||||
{
|
||||
delegate.set_status(Some("Building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(
|
||||
Command::new("cargo")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env("RUSTFLAGS", &rust_flags),
|
||||
)
|
||||
.await?;
|
||||
} else if build_remote_server.contains("cross") {
|
||||
#[cfg(target_os = "windows")]
|
||||
use util::paths::SanitizedPath;
|
||||
|
||||
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||
log::info!("installing cross");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"install",
|
||||
"cross",
|
||||
"--git",
|
||||
"https://github.com/cross-rs/cross",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
delegate.set_status(
|
||||
Some(&format!(
|
||||
"Building remote server binary from source for {} with Docker",
|
||||
&triple
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote server binary from source for {}", &triple);
|
||||
|
||||
// On Windows, the binding needs to be set to the canonical path
|
||||
#[cfg(target_os = "windows")]
|
||||
let src =
|
||||
SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let src = "./target";
|
||||
run_cmd(
|
||||
Command::new("cross")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env(
|
||||
"CROSS_CONTAINER_OPTS",
|
||||
format!("--mount type=bind,src={src},dst=/app/target"),
|
||||
)
|
||||
.env("RUSTFLAGS", &rust_flags),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let which = cx
|
||||
.background_spawn(async move { which::which("zig") })
|
||||
.await;
|
||||
|
||||
if which.is_err() {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
|
||||
log::info!("adding rustup target");
|
||||
run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
|
||||
|
||||
delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
|
||||
log::info!("installing cargo-zigbuild");
|
||||
run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
|
||||
|
||||
delegate.set_status(
|
||||
Some(&format!(
|
||||
"Building remote binary from source for {triple} with Zig"
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote binary from source for {triple} with Zig");
|
||||
run_cmd(
|
||||
Command::new("cargo")
|
||||
.args([
|
||||
"zigbuild",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env("RUSTFLAGS", &rust_flags),
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
let bin_path = Path::new("target")
|
||||
.join("remote_server")
|
||||
.join(&triple)
|
||||
.join("debug")
|
||||
.join("remote_server");
|
||||
|
||||
let path = if !build_remote_server.contains("nocompress") {
|
||||
delegate.set_status(Some("Compressing binary"), cx);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, we use 7z to compress the binary
|
||||
let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
|
||||
let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
|
||||
if smol::fs::metadata(&gz_path).await.is_ok() {
|
||||
smol::fs::remove_file(&gz_path).await?;
|
||||
}
|
||||
run_cmd(Command::new(seven_zip).args([
|
||||
"a",
|
||||
"-tgzip",
|
||||
&gz_path,
|
||||
&bin_path.to_string_lossy(),
|
||||
]))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut archive_path = bin_path;
|
||||
archive_path.set_extension("gz");
|
||||
std::env::current_dir()?.join(archive_path)
|
||||
} else {
|
||||
bin_path
|
||||
};
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl SshSocket {
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
use crate::{
|
||||
RemoteClientDelegate, RemotePlatform,
|
||||
remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
|
||||
};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
|
||||
use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use rpc::proto::Envelope;
|
||||
use smol::{fs, process};
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use util::paths::{PathStyle, RemotePathBuf};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct WslConnectionOptions {
|
||||
pub distro_name: String,
|
||||
pub user: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct WslRemoteConnection {
|
||||
remote_binary_path: Option<RemotePathBuf>,
|
||||
platform: RemotePlatform,
|
||||
shell: String,
|
||||
connection_options: WslConnectionOptions,
|
||||
}
|
||||
|
||||
impl WslRemoteConnection {
|
||||
pub(crate) async fn new(
|
||||
connection_options: WslConnectionOptions,
|
||||
delegate: Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
log::info!(
|
||||
"Connecting to WSL distro {} with user {:?}",
|
||||
connection_options.distro_name,
|
||||
connection_options.user
|
||||
);
|
||||
let (release_channel, version, commit) = cx.update(|cx| {
|
||||
(
|
||||
ReleaseChannel::global(cx),
|
||||
AppVersion::global(cx),
|
||||
AppCommitSha::try_global(cx),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut this = Self {
|
||||
connection_options,
|
||||
remote_binary_path: None,
|
||||
platform: RemotePlatform { os: "", arch: "" },
|
||||
shell: String::new(),
|
||||
};
|
||||
delegate.set_status(Some("Detecting WSL environment"), cx);
|
||||
this.platform = this.detect_platform().await?;
|
||||
this.shell = this.detect_shell().await?;
|
||||
this.remote_binary_path = Some(
|
||||
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
|
||||
.await?,
|
||||
);
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn detect_platform(&self) -> Result<RemotePlatform> {
|
||||
let arch_str = self.run_wsl_command("uname", &["-m"]).await?;
|
||||
let arch_str = arch_str.trim().to_string();
|
||||
let arch = match arch_str.as_str() {
|
||||
"x86_64" => "x86_64",
|
||||
"aarch64" | "arm64" => "aarch64",
|
||||
_ => "x86_64",
|
||||
};
|
||||
Ok(RemotePlatform { os: "linux", arch })
|
||||
}
|
||||
|
||||
async fn detect_shell(&self) -> Result<String> {
|
||||
Ok(self
|
||||
.run_wsl_command("sh", &["-c", "echo $SHELL"])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|shell_path| shell_path.trim().split('/').next_back().map(str::to_string))
|
||||
.unwrap_or_else(|| "bash".to_string()))
|
||||
}
|
||||
|
||||
async fn windows_path_to_wsl_path(&self, source: &Path) -> Result<String> {
|
||||
windows_path_to_wsl_path_impl(&self.connection_options, source).await
|
||||
}
|
||||
|
||||
fn wsl_command(&self, program: &str, args: &[&str]) -> process::Command {
|
||||
wsl_command_impl(&self.connection_options, program, args)
|
||||
}
|
||||
|
||||
async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<String> {
|
||||
run_wsl_command_impl(&self.connection_options, program, args).await
|
||||
}
|
||||
|
||||
async fn ensure_server_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
release_channel: ReleaseChannel,
|
||||
version: SemanticVersion,
|
||||
commit: Option<AppCommitSha>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<RemotePathBuf> {
|
||||
let version_str = match release_channel {
|
||||
ReleaseChannel::Nightly => {
|
||||
let commit = commit.map(|s| s.full()).unwrap_or_default();
|
||||
format!("{}-{}", version, commit)
|
||||
}
|
||||
ReleaseChannel::Dev => "build".to_string(),
|
||||
_ => version.to_string(),
|
||||
};
|
||||
|
||||
let binary_name = format!(
|
||||
"zed-remote-server-{}-{}",
|
||||
release_channel.dev_name(),
|
||||
version_str
|
||||
);
|
||||
|
||||
let dst_path = RemotePathBuf::new(
|
||||
paths::remote_wsl_server_dir_relative().join(binary_name),
|
||||
PathStyle::Posix,
|
||||
);
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(remote_server_path) =
|
||||
super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await?
|
||||
{
|
||||
let tmp_path = RemotePathBuf::new(
|
||||
paths::remote_wsl_server_dir_relative().join(format!(
|
||||
"download-{}-{}",
|
||||
std::process::id(),
|
||||
remote_server_path.file_name().unwrap().to_string_lossy()
|
||||
)),
|
||||
PathStyle::Posix,
|
||||
);
|
||||
self.upload_file(&remote_server_path, &tmp_path, delegate, cx)
|
||||
.await?;
|
||||
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
|
||||
.await?;
|
||||
return Ok(dst_path);
|
||||
}
|
||||
|
||||
if self
|
||||
.run_wsl_command(&dst_path.to_string(), &["version"])
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(dst_path);
|
||||
}
|
||||
|
||||
delegate.set_status(Some("Installing remote server"), cx);
|
||||
|
||||
let wanted_version = match release_channel {
|
||||
ReleaseChannel::Nightly => None,
|
||||
ReleaseChannel::Dev => {
|
||||
return Err(anyhow!("Dev builds require manual installation"));
|
||||
}
|
||||
_ => Some(cx.update(|cx| AppVersion::global(cx))?),
|
||||
};
|
||||
|
||||
let src_path = delegate
|
||||
.download_server_binary_locally(self.platform, release_channel, wanted_version, cx)
|
||||
.await?;
|
||||
|
||||
let tmp_path = RemotePathBuf::new(
|
||||
PathBuf::from(format!("{}.{}.tmp", dst_path, std::process::id())),
|
||||
PathStyle::Posix,
|
||||
);
|
||||
|
||||
self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
|
||||
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
|
||||
.await?;
|
||||
|
||||
Ok(dst_path)
|
||||
}
|
||||
|
||||
async fn upload_file(
|
||||
&self,
|
||||
src_path: &Path,
|
||||
dst_path: &RemotePathBuf,
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
delegate.set_status(Some("Uploading remote server to WSL"), cx);
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?;
|
||||
}
|
||||
|
||||
let t0 = Instant::now();
|
||||
let src_stat = fs::metadata(&src_path).await?;
|
||||
let size = src_stat.len();
|
||||
log::info!(
|
||||
"uploading remote server to WSL {:?} ({}kb)",
|
||||
dst_path,
|
||||
size / 1024
|
||||
);
|
||||
|
||||
let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
|
||||
self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to copy file {}({}) to WSL {:?}: {}",
|
||||
src_path.display(),
|
||||
src_path_in_wsl,
|
||||
dst_path,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
log::info!("uploaded remote server in {:?}", t0.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_and_install(
|
||||
&self,
|
||||
tmp_path: &RemotePathBuf,
|
||||
dst_path: &RemotePathBuf,
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
delegate.set_status(Some("Extracting remote server"), cx);
|
||||
|
||||
let tmp_path_str = tmp_path.to_string();
|
||||
let dst_path_str = dst_path.to_string();
|
||||
|
||||
// Build extraction script with proper error handling
|
||||
let script = if tmp_path_str.ends_with(".gz") {
|
||||
let uncompressed = tmp_path_str.trim_end_matches(".gz");
|
||||
format!(
|
||||
"set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'",
|
||||
tmp_path_str, uncompressed, uncompressed, dst_path_str
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"set -e; chmod 755 '{}' && mv -f '{}' '{}'",
|
||||
tmp_path_str, tmp_path_str, dst_path_str
|
||||
)
|
||||
};
|
||||
|
||||
self.run_wsl_command("sh", &["-c", &script])
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to extract server binary: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl RemoteConnection for WslRemoteConnection {
|
||||
fn start_proxy(
|
||||
&self,
|
||||
unique_identifier: String,
|
||||
reconnect: bool,
|
||||
incoming_tx: UnboundedSender<Envelope>,
|
||||
outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
connection_activity_tx: Sender<()>,
|
||||
delegate: Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<i32>> {
|
||||
delegate.set_status(Some("Starting proxy"), cx);
|
||||
|
||||
let Some(remote_binary_path) = &self.remote_binary_path else {
|
||||
return Task::ready(Err(anyhow!("Remote binary path not set")));
|
||||
};
|
||||
|
||||
let mut proxy_command = format!(
|
||||
"exec {} proxy --identifier {}",
|
||||
remote_binary_path, unique_identifier
|
||||
);
|
||||
|
||||
if reconnect {
|
||||
proxy_command.push_str(" --reconnect");
|
||||
}
|
||||
|
||||
for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
|
||||
if let Some(value) = std::env::var(env_var).ok() {
|
||||
proxy_command = format!("{}='{}' {}", env_var, value, proxy_command);
|
||||
}
|
||||
}
|
||||
let proxy_process = match self
|
||||
.wsl_command("sh", &["-lc", &proxy_command])
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
{
|
||||
Ok(process) => process,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error)));
|
||||
}
|
||||
};
|
||||
|
||||
super::handle_rpc_messages_over_child_process_stdio(
|
||||
proxy_process,
|
||||
incoming_tx,
|
||||
outgoing_rx,
|
||||
connection_activity_tx,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn upload_directory(
|
||||
&self,
|
||||
src_path: PathBuf,
|
||||
dest_path: RemotePathBuf,
|
||||
cx: &App,
|
||||
) -> Task<Result<()>> {
|
||||
cx.background_spawn({
|
||||
let options = self.connection_options.clone();
|
||||
async move {
|
||||
let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?;
|
||||
|
||||
run_wsl_command_impl(&options, "cp", &["-r", &wsl_src, &dest_path.to_string()])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"failed to upload directory {} -> {}: {}",
|
||||
src_path.display(),
|
||||
dest_path.to_string(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_been_killed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn shares_network_interface(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn build_command(
|
||||
&self,
|
||||
program: Option<String>,
|
||||
args: &[String],
|
||||
env: &HashMap<String, String>,
|
||||
working_dir: Option<String>,
|
||||
port_forward: Option<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate> {
|
||||
if port_forward.is_some() {
|
||||
bail!("WSL shares the network interface with the host system");
|
||||
}
|
||||
|
||||
let working_dir = working_dir
|
||||
.map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string())
|
||||
.unwrap_or("~".to_string());
|
||||
|
||||
let mut script = String::new();
|
||||
|
||||
for (k, v) in env.iter() {
|
||||
write!(&mut script, "{}='{}' ", k, v).unwrap();
|
||||
}
|
||||
|
||||
if let Some(program) = program {
|
||||
let command = shlex::try_quote(&program)?;
|
||||
script.push_str(&command);
|
||||
for arg in args {
|
||||
let arg = shlex::try_quote(&arg)?;
|
||||
script.push_str(" ");
|
||||
script.push_str(&arg);
|
||||
}
|
||||
} else {
|
||||
write!(&mut script, "exec {} -l", self.shell).unwrap();
|
||||
}
|
||||
|
||||
let wsl_args = if let Some(user) = &self.connection_options.user {
|
||||
vec![
|
||||
"--distribution".to_string(),
|
||||
self.connection_options.distro_name.clone(),
|
||||
"--user".to_string(),
|
||||
user.clone(),
|
||||
"--cd".to_string(),
|
||||
working_dir,
|
||||
"--".to_string(),
|
||||
self.shell.clone(),
|
||||
"-c".to_string(),
|
||||
shlex::try_quote(&script)?.to_string(),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
"--distribution".to_string(),
|
||||
self.connection_options.distro_name.clone(),
|
||||
"--cd".to_string(),
|
||||
working_dir,
|
||||
"--".to_string(),
|
||||
self.shell.clone(),
|
||||
"-c".to_string(),
|
||||
shlex::try_quote(&script)?.to_string(),
|
||||
]
|
||||
};
|
||||
|
||||
Ok(CommandTemplate {
|
||||
program: "wsl.exe".to_string(),
|
||||
args: wsl_args,
|
||||
env: HashMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> RemoteConnectionOptions {
|
||||
RemoteConnectionOptions::Wsl(self.connection_options.clone())
|
||||
}
|
||||
|
||||
fn path_style(&self) -> PathStyle {
|
||||
PathStyle::Posix
|
||||
}
|
||||
|
||||
fn shell(&self) -> String {
|
||||
self.shell.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// `wslpath` is a executable available in WSL, it's a linux binary.
|
||||
/// So it doesn't support Windows style paths.
|
||||
async fn sanitize_path(path: &Path) -> Result<String> {
|
||||
let path = smol::fs::canonicalize(path).await?;
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
|
||||
Ok(sanitized.replace('\\', "/"))
|
||||
}
|
||||
|
||||
async fn windows_path_to_wsl_path_impl(
|
||||
options: &WslConnectionOptions,
|
||||
source: &Path,
|
||||
) -> Result<String> {
|
||||
let source = sanitize_path(source).await?;
|
||||
run_wsl_command_impl(options, "wslpath", &["-u", &source]).await
|
||||
}
|
||||
|
||||
fn wsl_command_impl(
|
||||
options: &WslConnectionOptions,
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
) -> process::Command {
|
||||
let mut command = util::command::new_smol_command("wsl.exe");
|
||||
|
||||
if let Some(user) = &options.user {
|
||||
command.arg("--user").arg(user);
|
||||
}
|
||||
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.arg("--distribution")
|
||||
.arg(&options.distro_name)
|
||||
.arg("--cd")
|
||||
.arg("~")
|
||||
.arg(program)
|
||||
.args(args);
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
async fn run_wsl_command_impl(
|
||||
options: &WslConnectionOptions,
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
) -> Result<String> {
|
||||
let output = wsl_command_impl(options, program, args).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Command '{}' failed: {}",
|
||||
program,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
@@ -32,6 +32,7 @@ use gpui::{
|
||||
use keymap_editor;
|
||||
use onboarding_banner::OnboardingBanner;
|
||||
use project::Project;
|
||||
use remote::RemoteConnectionOptions;
|
||||
use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
@@ -304,12 +305,14 @@ impl TitleBar {
|
||||
|
||||
fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let options = self.project.read(cx).remote_connection_options(cx)?;
|
||||
let host: SharedString = options.connection_string().into();
|
||||
let host: SharedString = options.display_name().into();
|
||||
|
||||
let nickname = options
|
||||
.nickname
|
||||
.map(|nick| nick.into())
|
||||
.unwrap_or_else(|| host.clone());
|
||||
let nickname = if let RemoteConnectionOptions::Ssh(options) = options {
|
||||
options.nickname.map(|nick| nick.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let nickname = nickname.unwrap_or_else(|| host.clone());
|
||||
|
||||
let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
|
||||
remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
|
||||
|
||||
+310
-128
@@ -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()
|
||||
|
||||
@@ -12,7 +12,7 @@ use db::sqlez::{
|
||||
use gpui::{AsyncWindowContext, Entity, WeakEntity};
|
||||
|
||||
use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use remote::RemoteConnectionOptions;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
@@ -24,19 +24,18 @@ use uuid::Uuid;
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub(crate) struct SshConnectionId(pub u64);
|
||||
pub(crate) struct RemoteConnectionId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct SerializedSshConnection {
|
||||
pub host: String,
|
||||
pub port: Option<u16>,
|
||||
pub user: Option<String>,
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub(crate) enum RemoteConnectionKind {
|
||||
Ssh,
|
||||
Wsl,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum SerializedWorkspaceLocation {
|
||||
Local,
|
||||
Ssh(SerializedSshConnection),
|
||||
Remote(RemoteConnectionOptions),
|
||||
}
|
||||
|
||||
impl SerializedWorkspaceLocation {
|
||||
@@ -68,6 +67,23 @@ pub struct DockStructure {
|
||||
pub(crate) bottom: DockData,
|
||||
}
|
||||
|
||||
impl RemoteConnectionKind {
|
||||
pub(crate) fn serialize(&self) -> &'static str {
|
||||
match self {
|
||||
RemoteConnectionKind::Ssh => "ssh",
|
||||
RemoteConnectionKind::Wsl => "wsl",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize(text: &str) -> Option<Self> {
|
||||
match text {
|
||||
"ssh" => Some(Self::Ssh),
|
||||
"wsl" => Some(Self::Wsl),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockStructure {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (left, next_index) = DockData::column(statement, start_index)?;
|
||||
|
||||
@@ -67,14 +67,14 @@ pub use pane_group::*;
|
||||
use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
|
||||
pub use persistence::{
|
||||
DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
|
||||
model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation},
|
||||
model::{ItemId, SerializedWorkspaceLocation},
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
|
||||
};
|
||||
use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use session::AppSession;
|
||||
@@ -5262,14 +5262,7 @@ impl Workspace {
|
||||
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
|
||||
let paths = PathList::new(&self.root_paths(cx));
|
||||
if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
|
||||
WorkspaceLocation::Location(
|
||||
SerializedWorkspaceLocation::Ssh(SerializedSshConnection {
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
user: connection.username,
|
||||
}),
|
||||
paths,
|
||||
)
|
||||
WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
|
||||
} else if self.project.read(cx).is_local() {
|
||||
if !paths.is_empty() {
|
||||
WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
|
||||
@@ -7282,9 +7275,9 @@ pub fn create_and_open_local_file(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_ssh_project_with_new_connection(
|
||||
pub fn open_remote_project_with_new_connection(
|
||||
window: WindowHandle<Workspace>,
|
||||
connection_options: SshConnectionOptions,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
cancel_rx: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn RemoteClientDelegate>,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -7293,11 +7286,11 @@ pub fn open_ssh_project_with_new_connection(
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let (workspace_id, serialized_workspace) =
|
||||
serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
|
||||
let session = match cx
|
||||
.update(|cx| {
|
||||
remote::RemoteClient::ssh(
|
||||
remote::RemoteClient::new(
|
||||
ConnectionIdentifier::Workspace(workspace_id.0),
|
||||
connection_options,
|
||||
cancel_rx,
|
||||
@@ -7323,7 +7316,7 @@ pub fn open_ssh_project_with_new_connection(
|
||||
)
|
||||
})?;
|
||||
|
||||
open_ssh_project_inner(
|
||||
open_remote_project_inner(
|
||||
project,
|
||||
paths,
|
||||
workspace_id,
|
||||
@@ -7336,8 +7329,8 @@ pub fn open_ssh_project_with_new_connection(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_ssh_project_with_existing_connection(
|
||||
connection_options: SshConnectionOptions,
|
||||
pub fn open_remote_project_with_existing_connection(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
project: Entity<Project>,
|
||||
paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -7346,9 +7339,9 @@ pub fn open_ssh_project_with_existing_connection(
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let (workspace_id, serialized_workspace) =
|
||||
serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
|
||||
open_ssh_project_inner(
|
||||
open_remote_project_inner(
|
||||
project,
|
||||
paths,
|
||||
workspace_id,
|
||||
@@ -7361,7 +7354,7 @@ pub fn open_ssh_project_with_existing_connection(
|
||||
})
|
||||
}
|
||||
|
||||
async fn open_ssh_project_inner(
|
||||
async fn open_remote_project_inner(
|
||||
project: Entity<Project>,
|
||||
paths: Vec<PathBuf>,
|
||||
workspace_id: WorkspaceId,
|
||||
@@ -7448,22 +7441,18 @@ async fn open_ssh_project_inner(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_ssh_project(
|
||||
connection_options: SshConnectionOptions,
|
||||
fn serialize_remote_project(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
|
||||
cx.background_spawn(async move {
|
||||
let ssh_connection_id = persistence::DB
|
||||
.get_or_create_ssh_connection(
|
||||
connection_options.host.clone(),
|
||||
connection_options.port,
|
||||
connection_options.username.clone(),
|
||||
)
|
||||
let remote_connection_id = persistence::DB
|
||||
.get_or_create_remote_connection(connection_options)
|
||||
.await?;
|
||||
|
||||
let serialized_workspace =
|
||||
persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id);
|
||||
persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
|
||||
|
||||
let workspace_id = if let Some(workspace_id) =
|
||||
serialized_workspace.as_ref().map(|workspace| workspace.id)
|
||||
@@ -8013,22 +8002,20 @@ pub struct WorkspacePosition {
|
||||
pub centered_layout: bool,
|
||||
}
|
||||
|
||||
pub fn ssh_workspace_position_from_db(
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
user: Option<String>,
|
||||
pub fn remote_workspace_position_from_db(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
paths_to_open: &[PathBuf],
|
||||
cx: &App,
|
||||
) -> Task<Result<WorkspacePosition>> {
|
||||
let paths = paths_to_open.to_vec();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let ssh_connection_id = persistence::DB
|
||||
.get_or_create_ssh_connection(host, port, user)
|
||||
let remote_connection_id = persistence::DB
|
||||
.get_or_create_remote_connection(connection_options)
|
||||
.await
|
||||
.context("fetching serialized ssh project")?;
|
||||
let serialized_workspace =
|
||||
persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id);
|
||||
persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
|
||||
|
||||
let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
|
||||
(Some(WindowBounds::Windowed(bounds)), None)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
if [ "$ZED_WSL_DEBUG_INFO" = true ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
ZED_PATH="$(dirname "$(realpath "$0")")"
|
||||
|
||||
IN_WSL=false
|
||||
if [ -n "$WSL_DISTRO_NAME" ]; then
|
||||
# $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2
|
||||
IN_WSL=true
|
||||
fi
|
||||
|
||||
if [ $IN_WSL = true ]; then
|
||||
WSL_USER="$USER"
|
||||
if [ -z "$WSL_USER" ]; then
|
||||
WSL_USER="$USERNAME"
|
||||
fi
|
||||
"$ZED_PATH/zed.exe" --wsl "$WSL_USER@$WSL_DISTRO_NAME" "$@"
|
||||
exit $?
|
||||
else
|
||||
echo "Only WSL is supported for now" >&2
|
||||
exit 1
|
||||
fi
|
||||
+42
-33
@@ -23,13 +23,14 @@ use http_client::{Url, read_proxy_from_env};
|
||||
use language::LanguageRegistry;
|
||||
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
||||
use prompt_store::PromptBuilder;
|
||||
use remote::RemoteConnectionOptions;
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
use assets::Assets;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use recent_projects::{SshSettings, open_ssh_project};
|
||||
use recent_projects::{SshSettings, open_remote_project};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use session::{AppSession, Session};
|
||||
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
||||
@@ -360,6 +361,7 @@ pub fn main() {
|
||||
open_listener.open(RawOpenRequest {
|
||||
urls,
|
||||
diff_paths: Vec::new(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -696,7 +698,7 @@ pub fn main() {
|
||||
let urls: Vec<_> = args
|
||||
.paths_or_urls
|
||||
.iter()
|
||||
.filter_map(|arg| parse_url_arg(arg, cx).log_err())
|
||||
.map(|arg| parse_url_arg(arg, cx))
|
||||
.collect();
|
||||
|
||||
let diff_paths: Vec<[String; 2]> = args
|
||||
@@ -706,7 +708,11 @@ pub fn main() {
|
||||
.collect();
|
||||
|
||||
if !urls.is_empty() || !diff_paths.is_empty() {
|
||||
open_listener.open(RawOpenRequest { urls, diff_paths })
|
||||
open_listener.open(RawOpenRequest {
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl: args.wsl,
|
||||
})
|
||||
}
|
||||
|
||||
match open_rx
|
||||
@@ -792,10 +798,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(connection_options) = request.ssh_connection {
|
||||
if let Some(connection_options) = request.remote_connection {
|
||||
cx.spawn(async move |cx| {
|
||||
let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();
|
||||
open_ssh_project(
|
||||
open_remote_project(
|
||||
connection_options,
|
||||
paths,
|
||||
app_state,
|
||||
@@ -978,31 +984,24 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
SerializedWorkspaceLocation::Ssh(ssh) => {
|
||||
SerializedWorkspaceLocation::Remote(mut connection_options) => {
|
||||
let app_state = app_state.clone();
|
||||
let ssh_host = ssh.host.clone();
|
||||
let task = cx.spawn(async move |cx| {
|
||||
let connection_options = cx.update(|cx| {
|
||||
if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
|
||||
cx.update(|cx| {
|
||||
SshSettings::get_global(cx)
|
||||
.connection_options_for(ssh.host, ssh.port, ssh.user)
|
||||
});
|
||||
|
||||
match connection_options {
|
||||
Ok(connection_options) => recent_projects::open_ssh_project(
|
||||
connection_options,
|
||||
paths.paths().into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e)),
|
||||
Err(e) => Err(anyhow::anyhow!(
|
||||
"Failed to get SSH connection options for {}: {}",
|
||||
ssh_host,
|
||||
e
|
||||
)),
|
||||
}
|
||||
.fill_connection_options_from_settings(options)
|
||||
})?;
|
||||
}
|
||||
let task = cx.spawn(async move |cx| {
|
||||
recent_projects::open_remote_project(
|
||||
connection_options,
|
||||
paths.paths().into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
@@ -1184,6 +1183,16 @@ struct Args {
|
||||
#[arg(long, value_name = "DIR")]
|
||||
user_data_dir: Option<String>,
|
||||
|
||||
/// The username and WSL distribution to use when opening paths. ,If not specified,
|
||||
/// Zed will attempt to open the paths directly.
|
||||
///
|
||||
/// The username is optional, and if not specified, the default user for the distribution
|
||||
/// will be used.
|
||||
///
|
||||
/// Example: `me@Ubuntu` or `Ubuntu` for default distribution.
|
||||
#[arg(long, value_name = "USER@DISTRO")]
|
||||
wsl: Option<String>,
|
||||
|
||||
/// Instructs zed to run as a dev server on this machine. (not implemented)
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
@@ -1242,18 +1251,18 @@ impl ToString for IdType {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_url_arg(arg: &str, cx: &App) -> Result<String> {
|
||||
fn parse_url_arg(arg: &str, cx: &App) -> String {
|
||||
match std::fs::canonicalize(Path::new(&arg)) {
|
||||
Ok(path) => Ok(format!("file://{}", path.display())),
|
||||
Err(error) => {
|
||||
Ok(path) => format!("file://{}", path.display()),
|
||||
Err(_) => {
|
||||
if arg.starts_with("file://")
|
||||
|| arg.starts_with("zed-cli://")
|
||||
|| arg.starts_with("ssh://")
|
||||
|| parse_zed_link(arg, cx).is_some()
|
||||
{
|
||||
Ok(arg.into())
|
||||
arg.into()
|
||||
} else {
|
||||
anyhow::bail!("error parsing path argument: {error}")
|
||||
format!("file://{arg}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ use project::{DirectoryLister, ProjectItem};
|
||||
use project_panel::ProjectPanel;
|
||||
use prompt_store::PromptBuilder;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
use recent_projects::open_ssh_project;
|
||||
use recent_projects::open_remote_project;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use rope::Rope;
|
||||
use search::project_search::ProjectSearchBar;
|
||||
@@ -1557,7 +1557,7 @@ pub fn open_new_ssh_project_from_project(
|
||||
};
|
||||
let connection_options = ssh_client.read(cx).connection_options();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
open_ssh_project(
|
||||
open_remote_project(
|
||||
connection_options,
|
||||
paths,
|
||||
app_state,
|
||||
|
||||
@@ -17,8 +17,8 @@ use gpui::{App, AsyncApp, Global, WindowHandle};
|
||||
use language::Point;
|
||||
use onboarding::FIRST_OPEN;
|
||||
use onboarding::show_onboarding_view;
|
||||
use recent_projects::{SshSettings, open_ssh_project};
|
||||
use remote::SshConnectionOptions;
|
||||
use recent_projects::{SshSettings, open_remote_project};
|
||||
use remote::{RemoteConnectionOptions, WslConnectionOptions};
|
||||
use settings::Settings;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -37,7 +37,7 @@ pub struct OpenRequest {
|
||||
pub diff_paths: Vec<[String; 2]>,
|
||||
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
||||
pub join_channel: Option<u64>,
|
||||
pub ssh_connection: Option<SshConnectionOptions>,
|
||||
pub remote_connection: Option<RemoteConnectionOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -51,6 +51,23 @@ pub enum OpenRequestKind {
|
||||
impl OpenRequest {
|
||||
pub fn parse(request: RawOpenRequest, cx: &App) -> Result<Self> {
|
||||
let mut this = Self::default();
|
||||
|
||||
this.diff_paths = request.diff_paths;
|
||||
if let Some(wsl) = request.wsl {
|
||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||
if user.is_empty() {
|
||||
anyhow::bail!("user is empty in wsl argument");
|
||||
}
|
||||
(Some(user.to_string()), distro.to_string())
|
||||
} else {
|
||||
(None, wsl)
|
||||
};
|
||||
this.remote_connection = Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
|
||||
distro_name,
|
||||
user,
|
||||
}));
|
||||
}
|
||||
|
||||
for url in request.urls {
|
||||
if let Some(server_name) = url.strip_prefix("zed-cli://") {
|
||||
this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?));
|
||||
@@ -80,8 +97,6 @@ impl OpenRequest {
|
||||
}
|
||||
}
|
||||
|
||||
this.diff_paths = request.diff_paths;
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
@@ -108,13 +123,15 @@ impl OpenRequest {
|
||||
if let Some(password) = url.password() {
|
||||
connection_options.password = Some(password.to_string());
|
||||
}
|
||||
if let Some(ssh_connection) = &self.ssh_connection {
|
||||
|
||||
let connection_options = RemoteConnectionOptions::Ssh(connection_options);
|
||||
if let Some(ssh_connection) = &self.remote_connection {
|
||||
anyhow::ensure!(
|
||||
*ssh_connection == connection_options,
|
||||
"cannot open multiple ssh connections"
|
||||
"cannot open multiple different remote connections"
|
||||
);
|
||||
}
|
||||
self.ssh_connection = Some(connection_options);
|
||||
self.remote_connection = Some(connection_options);
|
||||
self.parse_file_path(url.path());
|
||||
Ok(())
|
||||
}
|
||||
@@ -152,6 +169,7 @@ pub struct OpenListener(UnboundedSender<RawOpenRequest>);
|
||||
pub struct RawOpenRequest {
|
||||
pub urls: Vec<String>,
|
||||
pub diff_paths: Vec<[String; 2]>,
|
||||
pub wsl: Option<String>,
|
||||
}
|
||||
|
||||
impl Global for OpenListener {}
|
||||
@@ -303,13 +321,21 @@ pub async fn handle_cli_connection(
|
||||
paths,
|
||||
diff_paths,
|
||||
wait,
|
||||
wsl,
|
||||
open_new_workspace,
|
||||
env,
|
||||
user_data_dir: _,
|
||||
} => {
|
||||
if !urls.is_empty() {
|
||||
cx.update(|cx| {
|
||||
match OpenRequest::parse(RawOpenRequest { urls, diff_paths }, cx) {
|
||||
match OpenRequest::parse(
|
||||
RawOpenRequest {
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl,
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
Ok(open_request) => {
|
||||
handle_open_request(open_request, app_state.clone(), cx);
|
||||
responses.send(CliResponse::Exit { status: 0 }).log_err();
|
||||
@@ -422,30 +448,26 @@ async fn open_workspaces(
|
||||
errored = true
|
||||
}
|
||||
}
|
||||
SerializedWorkspaceLocation::Ssh(ssh) => {
|
||||
SerializedWorkspaceLocation::Remote(mut connection) => {
|
||||
let app_state = app_state.clone();
|
||||
let connection_options = cx.update(|cx| {
|
||||
SshSettings::get_global(cx)
|
||||
.connection_options_for(ssh.host, ssh.port, ssh.user)
|
||||
});
|
||||
if let Ok(connection_options) = connection_options {
|
||||
cx.spawn(async move |cx| {
|
||||
open_ssh_project(
|
||||
connection_options,
|
||||
workspace_paths.paths().to_vec(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
// We don't set `errored` here if `open_ssh_project` fails, because for ssh projects, the
|
||||
// error is displayed in the window.
|
||||
} else {
|
||||
errored = false;
|
||||
if let RemoteConnectionOptions::Ssh(options) = &mut connection {
|
||||
cx.update(|cx| {
|
||||
SshSettings::get_global(cx)
|
||||
.fill_connection_options_from_settings(options)
|
||||
})?;
|
||||
}
|
||||
cx.spawn(async move |cx| {
|
||||
open_remote_project(
|
||||
connection,
|
||||
workspace_paths.paths().to_vec(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,6 +609,7 @@ mod tests {
|
||||
};
|
||||
use editor::Editor;
|
||||
use gpui::TestAppContext;
|
||||
use remote::SshConnectionOptions;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
@@ -609,8 +632,8 @@ mod tests {
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
request.ssh_connection.unwrap(),
|
||||
SshConnectionOptions {
|
||||
request.remote_connection.unwrap(),
|
||||
RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "localhost".into(),
|
||||
username: Some("me".into()),
|
||||
port: None,
|
||||
@@ -619,7 +642,7 @@ mod tests {
|
||||
port_forwards: None,
|
||||
nickname: None,
|
||||
upload_binary_over_ssh: false,
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(request.open_paths, vec!["/"]);
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
|
||||
urls,
|
||||
diff_paths,
|
||||
wait: false,
|
||||
wsl: args.wsl.clone(),
|
||||
open_new_workspace: None,
|
||||
env: None,
|
||||
user_data_dir: args.user_data_dir.clone(),
|
||||
|
||||
@@ -150,6 +150,7 @@ function CollectFiles {
|
||||
Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force
|
||||
Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force
|
||||
Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force
|
||||
Move-Item -Path "$innoDir\zed-wsl" -Destination "$innoDir\bin\zed" -Force
|
||||
Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force
|
||||
Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user