diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index a626122769..3abefac8e8 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -334,7 +334,7 @@ impl PromptEditor { EditorEvent::Edited { .. } => { if let Some(workspace) = window.root::().flatten() { workspace.update(cx, |workspace, cx| { - let is_via_ssh = workspace.project().read(cx).is_via_ssh(); + let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); workspace .client() diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index ffe4c6c251..c31a458c64 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -1161,7 +1161,7 @@ impl Room { let request = self.client.request(proto::ShareProject { room_id: self.id(), worktrees: project.read(cx).worktree_metadata_protos(cx), - is_ssh_project: project.read(cx).is_via_ssh(), + is_ssh_project: project.read(cx).is_via_remote_server(), }); cx.spawn(async move |this, cx| { diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 8ab6e6910c..6b46459a59 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -26,7 +26,7 @@ use project::{ debugger::session::ThreadId, lsp_store::{FormatTrigger, LspFormatTarget}, }; -use remote::SshRemoteClient; +use remote::RemoteClient; use remote_server::{HeadlessAppState, HeadlessProject}; use rpc::proto; use serde_json::json; @@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project( .await; // Set up project on remote FS - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let (project_a, worktree_id) = client_a .build_ssh_project(path!("/code/project1"), client_ssh, cx_a) .await; @@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches( .await; // Set up project on remote FS - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree("/project", serde_json::json!({ ".git":{} })) @@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let (project_a, _) = client_a .build_ssh_project("/project", client_ssh, cx_a) .await; @@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); let buffer_text = "let one = \"two\""; let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; @@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let (project_a, worktree_id) = client_a .build_ssh_project(path!("/project"), client_ssh, cx_a) .await; @@ -602,7 +602,7 @@ async fn test_remote_server_debugger( release_channel::init(SemanticVersion::default(), cx); dap_adapters::init(cx); }); - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -633,7 +633,7 @@ async fn test_remote_server_debugger( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let mut server = TestServer::start(server_cx.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; cx_a.update(|cx| { @@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries( release_channel::init(SemanticVersion::default(), cx); dap_adapters::init(cx); }); - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let mut server = TestServer::start(server_cx.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; cx_a.update(|cx| { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index fd5e3eefc1..eb7df28478 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -26,7 +26,7 @@ use node_runtime::NodeRuntime; use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; -use remote::SshRemoteClient; +use remote::RemoteClient; use rpc::{ RECEIVE_TIMEOUT, proto::{self, ChannelRole}, @@ -765,11 +765,11 @@ impl TestClient { pub async fn build_ssh_project( &self, root_path: impl AsRef, - ssh: Entity, + ssh: Entity, cx: &mut TestAppContext, ) -> (Entity, WorktreeId) { let project = cx.update(|cx| { - Project::ssh( + Project::remote( ssh, self.client().clone(), self.app_state.node_runtime.clone(), diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 9991395f35..dd639ce434 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -918,7 +918,7 @@ impl RunningState { let weak_workspace = workspace.downgrade(); let ssh_info = project .read(cx) - .ssh_client() + .remote_client() .and_then(|it| it.read(cx).ssh_info()); cx.spawn_in(window, async move |this, cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 29e009fdf8..e6ca0bb2af 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20074,7 +20074,7 @@ impl Editor { let (telemetry, is_via_ssh) = { let project = project.read(cx); let telemetry = project.client().telemetry().clone(); - let is_via_ssh = project.is_via_ssh(); + let is_via_ssh = project.is_via_remote_server(); (telemetry, is_via_ssh) }; refresh_linked_ranges(self, window, cx); @@ -20642,7 +20642,7 @@ impl Editor { copilot_enabled, copilot_enabled_for_language, edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), + is_via_ssh = project.is_via_remote_server(), ); } else { telemetry::event!( @@ -20652,7 +20652,7 @@ impl Editor { copilot_enabled, copilot_enabled_for_language, edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), + is_via_ssh = project.is_via_remote_server(), ); }; } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index fde0aeac94..b8189c3651 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -43,7 +43,7 @@ use language::{ use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; -use remote::SshRemoteClient; +use remote::RemoteClient; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -117,7 +117,7 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, - pub ssh_clients: HashMap>, + pub remote_clients: HashMap>, pub ssh_registered_tx: UnboundedSender<()>, } @@ -270,7 +270,7 @@ impl ExtensionStore { reload_tx, tasks: Vec::new(), - ssh_clients: HashMap::default(), + remote_clients: HashMap::default(), ssh_registered_tx: connection_registered_tx, }; @@ -1693,7 +1693,7 @@ impl ExtensionStore { async fn sync_extensions_over_ssh( this: &WeakEntity, - client: WeakEntity, + client: WeakEntity, cx: &mut AsyncApp, ) -> Result<()> { let extensions = this.update(cx, |this, _cx| { @@ -1765,8 +1765,8 @@ impl ExtensionStore { pub async fn update_ssh_clients(this: &WeakEntity, cx: &mut AsyncApp) -> Result<()> { let clients = this.update(cx, |this, _cx| { - this.ssh_clients.retain(|_k, v| v.upgrade().is_some()); - this.ssh_clients.values().cloned().collect::>() + this.remote_clients.retain(|_k, v| v.upgrade().is_some()); + this.remote_clients.values().cloned().collect::>() })?; for client in clients { @@ -1778,17 +1778,17 @@ impl ExtensionStore { anyhow::Ok(()) } - pub fn register_ssh_client(&mut self, client: Entity, cx: &mut Context) { + pub fn register_remote_client(&mut self, client: Entity, cx: &mut Context) { let connection_options = client.read(cx).connection_options(); let ssh_url = connection_options.ssh_url(); - if let Some(existing_client) = self.ssh_clients.get(&ssh_url) + if let Some(existing_client) = self.remote_clients.get(&ssh_url) && existing_client.upgrade().is_some() { return; } - self.ssh_clients.insert(ssh_url, client.downgrade()); + self.remote_clients.insert(ssh_url, client.downgrade()); self.ssh_registered_tx.unbounded_send(()).ok(); } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 8aaaa04729..1a67760183 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1381,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate { project .worktree_for_id(history_item.project.worktree_id, cx) .is_some() - || ((project.is_local() || project.is_via_ssh()) + || ((project.is_local() || project.is_via_remote_server()) && history_item.absolute.is_some()) }), self.currently_opened_path.as_ref(), diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 43c0365291..3fc627efd9 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -222,7 +222,7 @@ pub fn init(cx: &mut App) { cx.observe_new(move |workspace: &mut Workspace, _, cx| { let project = workspace.project(); - if project.read(cx).is_local() || project.read(cx).is_via_ssh() { + if project.read(cx).is_local() || project.read(cx).is_via_remote_server() { log_store.update(cx, |store, cx| { store.add_project(project, cx); }); @@ -231,7 +231,7 @@ pub fn init(cx: &mut App) { let log_store = log_store.clone(); workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { let project = workspace.project().read(cx); - if project.is_local() || project.is_via_ssh() { + if project.is_local() || project.is_via_remote_server() { let project = workspace.project().clone(); let log_store = log_store.clone(); get_or_create_tool( @@ -321,7 +321,7 @@ impl LogStore { .retain(|_, state| state.kind.project() != Some(&weak_project)); }), cx.subscribe(project, |this, project, event, cx| { - let server_kind = if project.read(cx).is_via_ssh() { + let server_kind = if project.read(cx).is_via_remote_server() { LanguageServerKind::Remote { project: project.downgrade(), } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 10698cead8..1521d01295 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5102,9 +5102,9 @@ impl EventEmitter for OutlinePanel {} impl Render for OutlinePanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (is_local, is_via_ssh) = self - .project - .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh())); + let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| { + (project.is_local(), project.is_via_remote_server()) + }); let query = self.query(cx); let pinned = self.pinned; let settings = OutlinePanelSettings::get_global(cx); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2906c32ff4..33145baced 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -34,7 +34,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; +use remote::{RemoteClient, SshArgs, SshInfo}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -68,7 +68,7 @@ pub enum DapStoreEvent { enum DapStoreMode { Local(LocalDapStore), - Ssh(SshDapStore), + Remote(RemoteDapStore), Collab, } @@ -80,8 +80,8 @@ pub struct LocalDapStore { toolchain_store: Arc, } -pub struct SshDapStore { - ssh_client: Entity, +pub struct RemoteDapStore { + remote_client: Entity, upstream_client: AnyProtoClient, upstream_project_id: u64, } @@ -147,16 +147,16 @@ impl DapStore { Self::new(mode, breakpoint_store, worktree_store, cx) } - pub fn new_ssh( + pub fn new_remote( project_id: u64, - ssh_client: Entity, + remote_client: Entity, breakpoint_store: Entity, worktree_store: Entity, cx: &mut Context, ) -> Self { - let mode = DapStoreMode::Ssh(SshDapStore { - upstream_client: ssh_client.read(cx).proto_client(), - ssh_client, + let mode = DapStoreMode::Remote(RemoteDapStore { + upstream_client: remote_client.read(cx).proto_client(), + remote_client, upstream_project_id: project_id, }); @@ -242,20 +242,22 @@ impl DapStore { Ok(binary) }) } - DapStoreMode::Ssh(ssh) => { - let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary { - session_id: session_id.to_proto(), - project_id: ssh.upstream_project_id, - worktree_id: worktree.read(cx).id().to_proto(), - definition: Some(definition.to_proto()), - }); - let ssh_client = ssh.ssh_client.clone(); + DapStoreMode::Remote(remote) => { + let request = remote + .upstream_client + .request(proto::GetDebugAdapterBinary { + session_id: session_id.to_proto(), + project_id: remote.upstream_project_id, + worktree_id: worktree.read(cx).id().to_proto(), + definition: Some(definition.to_proto()), + }); + let remote = remote.remote_client.clone(); cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; let (mut ssh_command, envs, path_style, ssh_shell) = - ssh_client.read_with(cx, |ssh, _| { + remote.read_with(cx, |ssh, _| { let SshInfo { args: SshArgs { arguments, envs }, path_style, @@ -365,9 +367,9 @@ impl DapStore { ))) } } - DapStoreMode::Ssh(ssh) => { - let request = ssh.upstream_client.request(proto::RunDebugLocators { - project_id: ssh.upstream_project_id, + DapStoreMode::Remote(remote) => { + let request = remote.upstream_client.request(proto::RunDebugLocators { + project_id: remote.upstream_project_id, build_command: Some(build_command.to_proto()), locator: locator_name.to_owned(), }); diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5cf298a8bf..a1c0508c3e 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -44,7 +44,7 @@ use parking_lot::Mutex; use postage::stream::Stream as _; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update}, + proto::{self, FromProto, ToProto, git_reset, split_repository_update}, }; use serde::Deserialize; use std::{ @@ -141,14 +141,10 @@ enum GitStoreState { project_environment: Entity, fs: Arc, }, - Ssh { - upstream_client: AnyProtoClient, - upstream_project_id: ProjectId, - downstream: Option<(AnyProtoClient, ProjectId)>, - }, Remote { upstream_client: AnyProtoClient, - upstream_project_id: ProjectId, + upstream_project_id: u64, + downstream: Option<(AnyProtoClient, ProjectId)>, }, } @@ -355,7 +351,7 @@ impl GitStore { worktree_store: &Entity, buffer_store: Entity, upstream_client: AnyProtoClient, - project_id: ProjectId, + project_id: u64, cx: &mut Context, ) -> Self { Self::new( @@ -364,23 +360,6 @@ impl GitStore { GitStoreState::Remote { upstream_client, upstream_project_id: project_id, - }, - cx, - ) - } - - pub fn ssh( - worktree_store: &Entity, - buffer_store: Entity, - upstream_client: AnyProtoClient, - cx: &mut Context, - ) -> Self { - Self::new( - worktree_store.clone(), - buffer_store, - GitStoreState::Ssh { - upstream_client, - upstream_project_id: ProjectId(SSH_PROJECT_ID), downstream: None, }, cx, @@ -451,7 +430,7 @@ impl GitStore { pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { match &mut self.state { - GitStoreState::Ssh { + GitStoreState::Remote { downstream: downstream_client, .. } => { @@ -527,9 +506,6 @@ impl GitStore { }), }); } - GitStoreState::Remote { .. } => { - debug_panic!("shared called on remote store"); - } } } @@ -541,15 +517,12 @@ impl GitStore { } => { downstream_client.take(); } - GitStoreState::Ssh { + GitStoreState::Remote { downstream: downstream_client, .. } => { downstream_client.take(); } - GitStoreState::Remote { .. } => { - debug_panic!("unshared called on remote store"); - } } self.shared_diffs.clear(); } @@ -1047,21 +1020,17 @@ impl GitStore { } => downstream_client .as_ref() .map(|state| (state.client.clone(), state.project_id)), - GitStoreState::Ssh { + GitStoreState::Remote { downstream: downstream_client, .. } => downstream_client.clone(), - GitStoreState::Remote { .. } => None, } } fn upstream_client(&self) -> Option { match &self.state { GitStoreState::Local { .. } => None, - GitStoreState::Ssh { - upstream_client, .. - } - | GitStoreState::Remote { + GitStoreState::Remote { upstream_client, .. } => Some(upstream_client.clone()), } @@ -1432,12 +1401,7 @@ impl GitStore { cx.background_executor() .spawn(async move { fs.git_init(&path, fallback_branch_name) }) } - GitStoreState::Ssh { - upstream_client, - upstream_project_id: project_id, - .. - } - | GitStoreState::Remote { + GitStoreState::Remote { upstream_client, upstream_project_id: project_id, .. @@ -1447,7 +1411,7 @@ impl GitStore { cx.background_executor().spawn(async move { client .request(proto::GitInit { - project_id: project_id.0, + project_id: project_id, abs_path: path.to_string_lossy().to_string(), fallback_branch_name, }) @@ -1471,13 +1435,18 @@ impl GitStore { cx.background_executor() .spawn(async move { fs.git_clone(&repo, &path).await }) } - GitStoreState::Ssh { + GitStoreState::Remote { upstream_client, upstream_project_id, .. } => { + if upstream_client.is_via_collab() { + return Task::ready(Err(anyhow!( + "Git Clone isn't supported for project guests" + ))); + } let request = upstream_client.request(proto::GitClone { - project_id: upstream_project_id.0, + project_id: *upstream_project_id, abs_path: path.to_string_lossy().to_string(), remote_repo: repo, }); @@ -1491,9 +1460,6 @@ impl GitStore { } }) } - GitStoreState::Remote { .. } => { - Task::ready(Err(anyhow!("Git Clone isn't supported for remote users"))) - } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9fd4eed641..9e3900198c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -42,9 +42,7 @@ pub use manifest_tree::ManifestTree; use anyhow::{Context as _, Result, anyhow}; use buffer_store::{BufferStore, BufferStoreEvent}; -use client::{ - Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto, -}; +use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto}; use clock::ReplicaId; use dap::client::DebugAdapterClient; @@ -89,10 +87,10 @@ use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; -use remote::{SshConnectionOptions, SshRemoteClient}; +use remote::{RemoteClient, SshConnectionOptions}; use rpc::{ AnyProtoClient, ErrorCode, - proto::{FromProto, LanguageServerPromptResponse, SSH_PROJECT_ID, ToProto}, + proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; @@ -177,12 +175,12 @@ pub struct Project { dap_store: Entity, breakpoint_store: Entity, - client: Arc, + collab_client: Arc, join_project_response_message_id: u32, task_store: Entity, user_store: Entity, fs: Arc, - ssh_client: Option>, + remote_client: Option>, client_state: ProjectClientState, git_store: Entity, collaborators: HashMap, @@ -1154,12 +1152,12 @@ impl Project { active_entry: None, snippets, languages, - client, + collab_client: client, task_store, user_store, settings_observer, fs, - ssh_client: None, + remote_client: None, breakpoint_store, dap_store, @@ -1183,8 +1181,8 @@ impl Project { }) } - pub fn ssh( - ssh: Entity, + pub fn remote( + remote: Entity, client: Arc, node: NodeRuntime, user_store: Entity, @@ -1200,10 +1198,15 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); - let (ssh_proto, path_style) = - ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style())); + let (remote_proto, path_style) = + remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style())); let worktree_store = cx.new(|_| { - WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style) + WorktreeStore::remote( + false, + remote_proto.clone(), + REMOTE_SERVER_PROJECT_ID, + path_style, + ) }); cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); @@ -1215,31 +1218,32 @@ impl Project { let buffer_store = cx.new(|cx| { BufferStore::remote( worktree_store.clone(), - ssh.read(cx).proto_client(), - SSH_PROJECT_ID, + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, cx, ) }); let image_store = cx.new(|cx| { ImageStore::remote( worktree_store.clone(), - ssh.read(cx).proto_client(), - SSH_PROJECT_ID, + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, cx, ) }); cx.subscribe(&buffer_store, Self::on_buffer_store_event) .detach(); - let toolchain_store = cx - .new(|cx| ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx)); + let toolchain_store = cx.new(|cx| { + ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx) + }); let task_store = cx.new(|cx| { TaskStore::remote( fs.clone(), buffer_store.downgrade(), worktree_store.clone(), toolchain_store.read(cx).as_language_toolchain_store(), - ssh.read(cx).proto_client(), - SSH_PROJECT_ID, + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, cx, ) }); @@ -1262,8 +1266,8 @@ impl Project { buffer_store.clone(), worktree_store.clone(), languages.clone(), - ssh_proto.clone(), - SSH_PROJECT_ID, + remote_proto.clone(), + REMOTE_SERVER_PROJECT_ID, fs.clone(), cx, ) @@ -1271,12 +1275,12 @@ impl Project { cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); let breakpoint_store = - cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, ssh_proto.clone())); + cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone())); let dap_store = cx.new(|cx| { - DapStore::new_ssh( - SSH_PROJECT_ID, - ssh.clone(), + DapStore::new_remote( + REMOTE_SERVER_PROJECT_ID, + remote.clone(), breakpoint_store.clone(), worktree_store.clone(), cx, @@ -1284,10 +1288,16 @@ impl Project { }); let git_store = cx.new(|cx| { - GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx) + GitStore::remote( + &worktree_store, + buffer_store.clone(), + remote_proto.clone(), + REMOTE_SERVER_PROJECT_ID, + cx, + ) }); - cx.subscribe(&ssh, Self::on_ssh_event).detach(); + cx.subscribe(&remote, Self::on_remote_client_event).detach(); let this = Self { buffer_ordered_messages_tx: tx, @@ -1306,11 +1316,13 @@ impl Project { _subscriptions: vec![ cx.on_release(Self::release), cx.on_app_quit(|this, cx| { - let shutdown = this.ssh_client.take().and_then(|client| { - client.read(cx).shutdown_processes( - Some(proto::ShutdownRemoteServer {}), - cx.background_executor().clone(), - ) + let shutdown = this.remote_client.take().and_then(|client| { + client.update(cx, |client, cx| { + client.shutdown_processes( + Some(proto::ShutdownRemoteServer {}), + cx.background_executor().clone(), + ) + }) }); cx.background_executor().spawn(async move { @@ -1323,12 +1335,12 @@ impl Project { active_entry: None, snippets, languages, - client, + collab_client: client, task_store, user_store, settings_observer, fs, - ssh_client: Some(ssh.clone()), + remote_client: Some(remote.clone()), buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -1346,52 +1358,34 @@ impl Project { agent_location: None, }; - // ssh -> local machine handlers - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); + // remote server -> local machine handlers + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity()); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.buffer_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store); - ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); - ssh_proto.add_entity_message_handler(Self::handle_update_worktree); - ssh_proto.add_entity_message_handler(Self::handle_update_project); - ssh_proto.add_entity_message_handler(Self::handle_toast); - ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request); - ssh_proto.add_entity_message_handler(Self::handle_hide_toast); - ssh_proto.add_entity_request_handler(Self::handle_update_buffer_from_ssh); - BufferStore::init(&ssh_proto); - LspStore::init(&ssh_proto); - SettingsObserver::init(&ssh_proto); - TaskStore::init(Some(&ssh_proto)); - ToolchainStore::init(&ssh_proto); - DapStore::init(&ssh_proto, cx); - GitStore::init(&ssh_proto); + remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); + remote_proto.add_entity_message_handler(Self::handle_update_worktree); + remote_proto.add_entity_message_handler(Self::handle_update_project); + remote_proto.add_entity_message_handler(Self::handle_toast); + remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request); + remote_proto.add_entity_message_handler(Self::handle_hide_toast); + remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server); + BufferStore::init(&remote_proto); + LspStore::init(&remote_proto); + SettingsObserver::init(&remote_proto); + TaskStore::init(Some(&remote_proto)); + ToolchainStore::init(&remote_proto); + DapStore::init(&remote_proto, cx); + GitStore::init(&remote_proto); this }) } - pub async fn remote( - remote_id: u64, - client: Arc, - user_store: Entity, - languages: Arc, - fs: Arc, - cx: AsyncApp, - ) -> Result> { - let project = - Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?; - cx.update(|cx| { - connection_manager::Manager::global(cx).update(cx, |manager, cx| { - manager.maintain_project_connection(&project, cx) - }) - })?; - Ok(project) - } - pub async fn in_room( remote_id: u64, client: Arc, @@ -1523,7 +1517,7 @@ impl Project { &worktree_store, buffer_store.clone(), client.clone().into(), - ProjectId(remote_id), + remote_id, cx, ) })?; @@ -1574,11 +1568,11 @@ impl Project { task_store, snippets, fs, - ssh_client: None, + remote_client: None, settings_observer: settings_observer.clone(), client_subscriptions: Default::default(), _subscriptions: vec![cx.on_release(Self::release)], - client: client.clone(), + collab_client: client.clone(), client_state: ProjectClientState::Remote { sharing_has_stopped: false, capability: Capability::ReadWrite, @@ -1661,11 +1655,13 @@ impl Project { } fn release(&mut self, cx: &mut App) { - if let Some(client) = self.ssh_client.take() { - let shutdown = client.read(cx).shutdown_processes( - Some(proto::ShutdownRemoteServer {}), - cx.background_executor().clone(), - ); + if let Some(client) = self.remote_client.take() { + let shutdown = client.update(cx, |client, cx| { + client.shutdown_processes( + Some(proto::ShutdownRemoteServer {}), + cx.background_executor().clone(), + ) + }); cx.background_spawn(async move { if let Some(shutdown) = shutdown { @@ -1681,7 +1677,7 @@ impl Project { let _ = self.unshare_internal(cx); } ProjectClientState::Remote { remote_id, .. } => { - let _ = self.client.send(proto::LeaveProject { + let _ = self.collab_client.send(proto::LeaveProject { project_id: *remote_id, }); self.disconnected_from_host_internal(cx); @@ -1808,11 +1804,11 @@ impl Project { } pub fn client(&self) -> Arc { - self.client.clone() + self.collab_client.clone() } - pub fn ssh_client(&self) -> Option> { - self.ssh_client.clone() + pub fn remote_client(&self) -> Option> { + self.remote_client.clone() } pub fn user_store(&self) -> Entity { @@ -1893,30 +1889,30 @@ impl Project { if self.is_local() { return true; } - if self.is_via_ssh() { + if self.is_via_remote_server() { return true; } false } - pub fn ssh_connection_state(&self, cx: &App) -> Option { - self.ssh_client + pub fn remote_connection_state(&self, cx: &App) -> Option { + self.remote_client .as_ref() - .map(|ssh| ssh.read(cx).connection_state()) + .map(|remote| remote.read(cx).connection_state()) } - pub fn ssh_connection_options(&self, cx: &App) -> Option { - self.ssh_client + pub fn remote_connection_options(&self, cx: &App) -> Option { + self.remote_client .as_ref() - .map(|ssh| ssh.read(cx).connection_options()) + .map(|remote| remote.read(cx).connection_options()) } pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, _ => { - if self.ssh_client.is_some() { + if self.remote_client.is_some() { 1 } else { 0 @@ -2220,55 +2216,55 @@ impl Project { ); self.client_subscriptions.extend([ - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&cx.entity(), &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.worktree_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.buffer_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.lsp_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.settings_observer, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.dap_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.breakpoint_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.git_store, &cx.to_async()), ]); self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.shared(project_id, self.client.clone().into(), cx) + buffer_store.shared(project_id, self.collab_client.clone().into(), cx) }); self.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.shared(project_id, self.client.clone().into(), cx); + worktree_store.shared(project_id, self.collab_client.clone().into(), cx); }); self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.shared(project_id, self.client.clone().into(), cx) + lsp_store.shared(project_id, self.collab_client.clone().into(), cx) }); self.breakpoint_store.update(cx, |breakpoint_store, _| { - breakpoint_store.shared(project_id, self.client.clone().into()) + breakpoint_store.shared(project_id, self.collab_client.clone().into()) }); self.dap_store.update(cx, |dap_store, cx| { - dap_store.shared(project_id, self.client.clone().into(), cx); + dap_store.shared(project_id, self.collab_client.clone().into(), cx); }); self.task_store.update(cx, |task_store, cx| { - task_store.shared(project_id, self.client.clone().into(), cx); + task_store.shared(project_id, self.collab_client.clone().into(), cx); }); self.settings_observer.update(cx, |settings_observer, cx| { - settings_observer.shared(project_id, self.client.clone().into(), cx) + settings_observer.shared(project_id, self.collab_client.clone().into(), cx) }); self.git_store.update(cx, |git_store, cx| { - git_store.shared(project_id, self.client.clone().into(), cx) + git_store.shared(project_id, self.collab_client.clone().into(), cx) }); self.client_state = ProjectClientState::Shared { @@ -2293,7 +2289,7 @@ impl Project { }); if let Some(remote_id) = self.remote_id() { self.git_store.update(cx, |git_store, cx| { - git_store.shared(remote_id, self.client.clone().into(), cx) + git_store.shared(remote_id, self.collab_client.clone().into(), cx) }); } cx.emit(Event::Reshared); @@ -2370,7 +2366,7 @@ impl Project { git_store.unshared(cx); }); - self.client + self.collab_client .send(proto::UnshareProject { project_id: remote_id, }) @@ -2437,15 +2433,17 @@ impl Project { sharing_has_stopped, .. } => *sharing_has_stopped, - ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx), + ProjectClientState::Local if self.is_via_remote_server() => { + self.remote_client_is_disconnected(cx) + } _ => false, } } - fn ssh_is_disconnected(&self, cx: &App) -> bool { - self.ssh_client + fn remote_client_is_disconnected(&self, cx: &App) -> bool { + self.remote_client .as_ref() - .map(|ssh| ssh.read(cx).is_disconnected()) + .map(|remote| remote.read(cx).is_disconnected()) .unwrap_or(false) } @@ -2463,16 +2461,16 @@ impl Project { pub fn is_local(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { - self.ssh_client.is_none() + self.remote_client.is_none() } ProjectClientState::Remote { .. } => false, } } - pub fn is_via_ssh(&self) -> bool { + pub fn is_via_remote_server(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { - self.ssh_client.is_some() + self.remote_client.is_some() } ProjectClientState::Remote { .. } => false, } @@ -2496,7 +2494,7 @@ impl Project { language: Option>, cx: &mut Context, ) -> Entity { - if self.is_via_collab() || self.is_via_ssh() { + if self.is_via_collab() || self.is_via_remote_server() { panic!("called create_local_buffer on a remote project") } self.buffer_store.update(cx, |buffer_store, cx| { @@ -2620,10 +2618,10 @@ impl Project { ) -> Task>> { if let Some(buffer) = self.buffer_for_id(id, cx) { Task::ready(Ok(buffer)) - } else if self.is_local() || self.is_via_ssh() { + } else if self.is_local() || self.is_via_remote_server() { Task::ready(Err(anyhow!("buffer {id} does not exist"))) } else if let Some(project_id) = self.remote_id() { - let request = self.client.request(proto::OpenBufferById { + let request = self.collab_client.request(proto::OpenBufferById { project_id, id: id.into(), }); @@ -2741,7 +2739,7 @@ impl Project { for (buffer_id, operations) in operations_by_buffer_id.drain() { let request = this.read_with(cx, |this, _| { let project_id = this.remote_id()?; - Some(this.client.request(proto::UpdateBuffer { + Some(this.collab_client.request(proto::UpdateBuffer { buffer_id: buffer_id.into(), project_id, operations, @@ -2808,7 +2806,7 @@ impl Project { project.read_with(cx, |project, _| { if let Some(project_id) = project.remote_id() { project - .client + .collab_client .send(proto::UpdateLanguageServer { project_id, server_name: name.map(|name| String::from(name.0)), @@ -2846,8 +2844,8 @@ impl Project { self.register_buffer(buffer, cx).log_err(); } BufferStoreEvent::BufferDropped(buffer_id) => { - if let Some(ref ssh_client) = self.ssh_client { - ssh_client + if let Some(ref remote_client) = self.remote_client { + remote_client .read(cx) .proto_client() .send(proto::CloseBuffer { @@ -2995,16 +2993,14 @@ impl Project { } } - fn on_ssh_event( + fn on_remote_client_event( &mut self, - _: Entity, - event: &remote::SshRemoteEvent, + _: Entity, + event: &remote::RemoteClientEvent, cx: &mut Context, ) { match event { - remote::SshRemoteEvent::Disconnected => { - // if self.is_via_ssh() { - // self.collaborators.clear(); + remote::RemoteClientEvent::Disconnected => { self.worktree_store.update(cx, |store, cx| { store.disconnected_from_host(cx); }); @@ -3110,8 +3106,9 @@ impl Project { } fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context) { - if let Some(ssh) = &self.ssh_client { - ssh.read(cx) + if let Some(remote) = &self.remote_client { + remote + .read(cx) .proto_client() .send(proto::RemoveWorktree { worktree_id: id_to_remove.to_proto(), @@ -3144,8 +3141,9 @@ impl Project { } => { let operation = language::proto::serialize_operation(operation); - if let Some(ssh) = &self.ssh_client { - ssh.read(cx) + if let Some(remote) = &self.remote_client { + remote + .read(cx) .proto_client() .send(proto::UpdateBuffer { project_id: 0, @@ -3552,16 +3550,16 @@ impl Project { pub fn open_server_settings(&mut self, cx: &mut Context) -> Task>> { let guard = self.retain_remotely_created_models(cx); - let Some(ssh_client) = self.ssh_client.as_ref() else { + let Some(remote) = self.remote_client.as_ref() else { return Task::ready(Err(anyhow!("not an ssh project"))); }; - let proto_client = ssh_client.read(cx).proto_client(); + let proto_client = remote.read(cx).proto_client(); cx.spawn(async move |project, cx| { let buffer = proto_client .request(proto::OpenServerSettings { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, }) .await?; @@ -3948,10 +3946,11 @@ impl Project { ) -> Receiver> { let (tx, rx) = smol::channel::unbounded(); - let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client { + let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client + { (ssh_client.read(cx).proto_client(), 0) } else if let Some(remote_id) = self.remote_id() { - (self.client.clone().into(), remote_id) + (self.collab_client.clone().into(), remote_id) } else { return rx; }; @@ -4095,14 +4094,14 @@ impl Project { is_dir: metadata.is_dir, }) }) - } else if let Some(ssh_client) = self.ssh_client.as_ref() { + } else if let Some(ssh_client) = self.remote_client.as_ref() { let path_style = ssh_client.read(cx).path_style(); let request_path = RemotePathBuf::from_str(path, path_style); let request = ssh_client .read(cx) .proto_client() .request(proto::GetPathMetadata { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, path: request_path.to_proto(), }); cx.background_spawn(async move { @@ -4202,10 +4201,10 @@ impl Project { ) -> Task>> { if self.is_local() { DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx) - } else if let Some(session) = self.ssh_client.as_ref() { + } else if let Some(session) = self.remote_client.as_ref() { let path_buf = PathBuf::from(query); let request = proto::ListRemoteDirectory { - dev_server_id: SSH_PROJECT_ID, + dev_server_id: REMOTE_SERVER_PROJECT_ID, path: path_buf.to_proto(), config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }), }; @@ -4420,7 +4419,7 @@ impl Project { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - if this.is_local() || this.is_via_ssh() { + if this.is_local() || this.is_via_remote_server() { this.unshare(cx)?; } else { this.disconnected_from_host(cx); @@ -4629,7 +4628,7 @@ impl Project { })? } - async fn handle_update_buffer_from_ssh( + async fn handle_update_buffer_from_remote_server( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -4638,7 +4637,7 @@ impl Project { if let Some(remote_id) = this.remote_id() { let mut payload = envelope.payload.clone(); payload.project_id = remote_id; - cx.background_spawn(this.client.request(payload)) + cx.background_spawn(this.collab_client.request(payload)) .detach_and_log_err(cx); } this.buffer_store.clone() @@ -4652,9 +4651,9 @@ impl Project { cx: AsyncApp, ) -> Result { let buffer_store = this.read_with(&cx, |this, cx| { - if let Some(ssh) = &this.ssh_client { + if let Some(ssh) = &this.remote_client { let mut payload = envelope.payload.clone(); - payload.project_id = SSH_PROJECT_ID; + payload.project_id = REMOTE_SERVER_PROJECT_ID; cx.background_spawn(ssh.read(cx).proto_client().request(payload)) .detach_and_log_err(cx); } @@ -4704,7 +4703,7 @@ impl Project { mut cx: AsyncApp, ) -> Result { let response = this.update(&mut cx, |this, cx| { - let client = this.client.clone(); + let client = this.collab_client.clone(); this.buffer_store.update(cx, |this, cx| { this.handle_synchronize_buffers(envelope, cx, client) }) @@ -4841,7 +4840,7 @@ impl Project { } }; - let client = self.client.clone(); + let client = self.collab_client.clone(); cx.spawn(async move |this, cx| { let (buffers, incomplete_buffer_ids) = this.update(cx, |this, cx| { this.buffer_store.read(cx).buffer_version_info(cx) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b009b357fe..af6eab6f66 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; -use remote::{SshInfo, ssh_session::SshArgs}; +use remote::{SshArgs, SshInfo}; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -87,7 +87,7 @@ impl Project { } pub fn ssh_details(&self, cx: &App) -> Option { - if let Some(ssh_client) = &self.ssh_client { + if let Some(ssh_client) = &self.remote_client { let ssh_client = ssh_client.read(cx); if let Some(SshInfo { args: SshArgs { arguments, envs }, diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index b8905c73bc..9033415ca4 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -18,7 +18,7 @@ use gpui::{ use postage::oneshot; use rpc::{ AnyProtoClient, ErrorExt, TypedEnvelope, - proto::{self, FromProto, SSH_PROJECT_ID, ToProto}, + proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use smol::{ channel::{Receiver, Sender}, @@ -278,7 +278,7 @@ impl WorktreeStore { let path = RemotePathBuf::new(abs_path.into(), path_style); let response = client .request(proto::AddWorktree { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, path: path.to_proto(), visible, }) @@ -298,7 +298,7 @@ impl WorktreeStore { let worktree = cx.update(|cx| { Worktree::remote( - SSH_PROJECT_ID, + REMOTE_SERVER_PROJECT_ID, 0, proto::WorktreeMetadata { id: response.worktree_id, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c99f5f8172..7406b7b17b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -653,7 +653,7 @@ impl ProjectPanel { let file_path = entry.path.clone(); let worktree_id = worktree.read(cx).id(); let entry_id = entry.id; - let is_via_ssh = project.read(cx).is_via_ssh(); + let is_via_ssh = project.read(cx).is_via_remote_server(); workspace .open_path_preview( @@ -5295,7 +5295,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::open_system)) .on_action(cx.listener(Self::open_in_terminal)) }) - .when(project.is_via_ssh(), |el| { + .when(project.is_via_remote_server(), |el| { el.on_action(cx.listener(Self::open_in_terminal)) }) .on_mouse_down( diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d38e54685f..e17ec5203b 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -16,8 +16,8 @@ pub use typed_envelope::*; include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); -pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; -pub const SSH_PROJECT_ID: u64 = 0; +pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; +pub const REMOTE_SERVER_PROJECT_ID: u64 = 0; messages!( (Ack, Foreground), diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 8ffe0ef07c..36da6897b9 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -64,8 +64,8 @@ impl DisconnectedOverlay { } let handle = cx.entity().downgrade(); - let ssh_connection_options = project.read(cx).ssh_connection_options(cx); - let host = if let Some(ssh_connection_options) = ssh_connection_options { + 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) } else { Host::RemoteProject diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index a9c3284d0b..f4fd1f1c1b 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -28,8 +28,8 @@ use paths::user_ssh_config_file; use picker::Picker; use project::Fs; use project::Project; -use remote::ssh_session::ConnectionIdentifier; -use remote::{SshConnectionOptions, SshRemoteClient}; +use remote::remote_client::ConnectionIdentifier; +use remote::{RemoteClient, SshConnectionOptions}; use settings::Settings; use settings::SettingsStore; use settings::update_settings_file; @@ -69,7 +69,7 @@ pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakEntity, - retained_connections: Vec>, + retained_connections: Vec>, ssh_config_updates: Task<()>, ssh_config_servers: BTreeSet, create_new_window: bool, @@ -597,7 +597,7 @@ impl RemoteServerProjects { let (path_style, project) = cx.update(|_, cx| { ( session.read(cx).path_style(), - project::Project::ssh( + project::Project::remote( session, app_state.client.clone(), app_state.node_runtime.clone(), diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index d07ea48c7e..e3fb249d16 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -15,8 +15,9 @@ use gpui::{ use language::CursorShape; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; -use remote::ssh_session::{ConnectionIdentifier, SshPortForwardOption}; -use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient}; +use remote::{ + ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -451,7 +452,7 @@ pub struct SshClientDelegate { known_password: Option, } -impl remote::SshClientDelegate for SshClientDelegate { +impl remote::RemoteClientDelegate for SshClientDelegate { fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp) { let mut known_password = self.known_password.clone(); if let Some(password) = known_password.take() { @@ -473,7 +474,7 @@ impl remote::SshClientDelegate for SshClientDelegate { fn download_server_binary_locally( &self, - platform: SshPlatform, + platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, @@ -503,7 +504,7 @@ impl remote::SshClientDelegate for SshClientDelegate { fn get_download_params( &self, - platform: SshPlatform, + platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, @@ -543,13 +544,13 @@ pub fn connect_over_ssh( ui: Entity, window: &mut Window, cx: &mut App, -) -> Task>>> { +) -> Task>>> { let window = window.window_handle(); let known_password = connection_options.password.clone(); let (tx, rx) = oneshot::channel(); ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); - remote::SshRemoteClient::new( + remote::RemoteClient::ssh( unique_identifier, connection_options, rx, @@ -681,9 +682,9 @@ pub async fn open_ssh_project( window .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).ssh_client() { + if let Some(client) = workspace.project().read(cx).remote_client() { ExtensionStore::global(cx) - .update(cx, |store, cx| store.register_ssh_client(client, cx)); + .update(cx, |store, cx| store.register_remote_client(client, cx)); } }) .ok(); diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs index e5a9c5b7a5..867a31b164 100644 --- a/crates/remote/src/protocol.rs +++ b/crates/remote/src/protocol.rs @@ -51,6 +51,16 @@ pub async fn write_message( Ok(()) } +pub async fn write_size_prefixed_buffer( + stream: &mut S, + buffer: &mut Vec, +) -> Result<()> { + let len = buffer.len() as u32; + stream.write_all(len.to_le_bytes().as_slice()).await?; + stream.write_all(buffer).await?; + Ok(()) +} + pub async fn read_message_raw( stream: &mut S, buffer: &mut Vec, diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 71895f1678..9094d8ebc9 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -1,9 +1,11 @@ pub mod json_log; pub mod protocol; pub mod proxy; -pub mod ssh_session; +pub mod remote_client; +mod transport; -pub use ssh_session::{ - ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform, - SshRemoteClient, SshRemoteEvent, +pub use remote_client::{ + ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, + RemotePlatform, }; +pub use transport::ssh::{SshArgs, SshConnectionOptions, SshInfo, SshPortForwardOption}; diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/remote_client.rs similarity index 50% rename from crates/remote/src/ssh_session.rs rename to crates/remote/src/remote_client.rs index b9af528643..9b96db7964 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/remote_client.rs @@ -1,15 +1,14 @@ use crate::{ - json_log::LogRecord, - protocol::{ - MESSAGE_LEN_SIZE, MessageId, message_len_from_buffer, read_message_with_len, write_message, - }, + SshConnectionOptions, + protocol::MessageId, proxy::ProxyLaunchError, + transport::ssh::{SshArgs, SshInfo, SshRemoteConnection}, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; use collections::HashMap; use futures::{ - AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, + Future, FutureExt as _, StreamExt as _, channel::{ mpsc::{self, Sender, UnboundedReceiver, UnboundedSender}, oneshot, @@ -21,313 +20,47 @@ use gpui::{ App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity, EventEmitter, Global, SemanticVersion, Task, WeakEntity, }; -use itertools::Itertools; use parking_lot::Mutex; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use release_channel::ReleaseChannel; use rpc::{ AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, }; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use smol::{ - fs, - process::{self, Child, Stdio}, -}; use std::{ collections::VecDeque, - fmt, iter, + fmt, ops::ControlFlow, - path::{Path, PathBuf}, + path::PathBuf, sync::{ Arc, Weak, atomic::{AtomicU32, AtomicU64, Ordering::SeqCst}, }, time::{Duration, Instant}, }; -use tempfile::TempDir; use util::{ ResultExt, paths::{PathStyle, RemotePathBuf}, }; -#[derive(Clone)] -pub struct SshSocket { - connection_options: SshConnectionOptions, - #[cfg(not(target_os = "windows"))] - socket_path: PathBuf, - #[cfg(target_os = "windows")] - envs: HashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] -pub struct SshPortForwardOption { - #[serde(skip_serializing_if = "Option::is_none")] - pub local_host: Option, - pub local_port: u16, - #[serde(skip_serializing_if = "Option::is_none")] - pub remote_host: Option, - pub remote_port: u16, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct SshConnectionOptions { - pub host: String, - pub username: Option, - pub port: Option, - pub password: Option, - pub args: Option>, - pub port_forwards: Option>, - - pub nickname: Option, - pub upload_binary_over_ssh: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshArgs { - pub arguments: Vec, - pub envs: Option>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshInfo { - pub args: SshArgs, - pub path_style: PathStyle, - pub shell: String, -} - -#[macro_export] -macro_rules! shell_script { - ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ - format!( - $fmt, - $( - $name = shlex::try_quote($arg).unwrap() - ),+ - ) - }}; -} - -fn parse_port_number(port_str: &str) -> Result { - port_str - .parse() - .with_context(|| format!("parsing port number: {port_str}")) -} - -fn parse_port_forward_spec(spec: &str) -> Result { - let parts: Vec<&str> = spec.split(':').collect(); - - match parts.len() { - 4 => { - let local_port = parse_port_number(parts[1])?; - let remote_port = parse_port_number(parts[3])?; - - Ok(SshPortForwardOption { - local_host: Some(parts[0].to_string()), - local_port, - remote_host: Some(parts[2].to_string()), - remote_port, - }) - } - 3 => { - let local_port = parse_port_number(parts[0])?; - let remote_port = parse_port_number(parts[2])?; - - Ok(SshPortForwardOption { - local_host: None, - local_port, - remote_host: Some(parts[1].to_string()), - remote_port, - }) - } - _ => anyhow::bail!("Invalid port forward format"), - } -} - -impl SshConnectionOptions { - pub fn parse_command_line(input: &str) -> Result { - let input = input.trim_start_matches("ssh "); - let mut hostname: Option = None; - let mut username: Option = None; - let mut port: Option = None; - let mut args = Vec::new(); - let mut port_forwards: Vec = Vec::new(); - - // disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W - const ALLOWED_OPTS: &[&str] = &[ - "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", - ]; - const ALLOWED_ARGS: &[&str] = &[ - "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", - "-w", - ]; - - let mut tokens = shlex::split(input).context("invalid input")?.into_iter(); - - 'outer: while let Some(arg) = tokens.next() { - if ALLOWED_OPTS.contains(&(&arg as &str)) { - args.push(arg.to_string()); - continue; - } - if arg == "-p" { - port = tokens.next().and_then(|arg| arg.parse().ok()); - continue; - } else if let Some(p) = arg.strip_prefix("-p") { - port = p.parse().ok(); - continue; - } - if arg == "-l" { - username = tokens.next(); - continue; - } else if let Some(l) = arg.strip_prefix("-l") { - username = Some(l.to_string()); - continue; - } - if arg == "-L" || arg.starts_with("-L") { - let forward_spec = if arg == "-L" { - tokens.next() - } else { - Some(arg.strip_prefix("-L").unwrap().to_string()) - }; - - if let Some(spec) = forward_spec { - port_forwards.push(parse_port_forward_spec(&spec)?); - } else { - anyhow::bail!("Missing port forward format"); - } - } - - for a in ALLOWED_ARGS { - if arg == *a { - args.push(arg); - if let Some(next) = tokens.next() { - args.push(next); - } - continue 'outer; - } else if arg.starts_with(a) { - args.push(arg); - continue 'outer; - } - } - if arg.starts_with("-") || hostname.is_some() { - anyhow::bail!("unsupported argument: {:?}", arg); - } - let mut input = &arg as &str; - // Destination might be: username1@username2@ip2@ip1 - if let Some((u, rest)) = input.rsplit_once('@') { - input = rest; - username = Some(u.to_string()); - } - if let Some((rest, p)) = input.split_once(':') { - input = rest; - port = p.parse().ok() - } - hostname = Some(input.to_string()) - } - - let Some(hostname) = hostname else { - anyhow::bail!("missing hostname"); - }; - - let port_forwards = match port_forwards.len() { - 0 => None, - _ => Some(port_forwards), - }; - - Ok(Self { - host: hostname, - username, - port, - port_forwards, - args: Some(args), - password: None, - nickname: None, - upload_binary_over_ssh: false, - }) - } - - pub fn ssh_url(&self) -> String { - let mut result = String::from("ssh://"); - if let Some(username) = &self.username { - // Username might be: username1@username2@ip2 - let username = urlencoding::encode(username); - result.push_str(&username); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result - } - - pub fn additional_args(&self) -> Vec { - let mut args = self.args.iter().flatten().cloned().collect::>(); - - if let Some(forwards) = &self.port_forwards { - args.extend(forwards.iter().map(|pf| { - let local_host = match &pf.local_host { - Some(host) => host, - None => "localhost", - }; - let remote_host = match &pf.remote_host { - Some(host) => host, - None => "localhost", - }; - - format!( - "-L{}:{}:{}:{}", - local_host, pf.local_port, remote_host, pf.remote_port - ) - })); - } - - args - } - - fn scp_url(&self) -> String { - if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - } - } - - pub fn connection_string(&self) -> String { - let host = if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - }; - if let Some(port) = &self.port { - format!("{}:{}", host, port) - } else { - host - } - } -} - #[derive(Copy, Clone, Debug)] -pub struct SshPlatform { +pub struct RemotePlatform { pub os: &'static str, pub arch: &'static str, } -pub trait SshClientDelegate: Send + Sync { +pub trait RemoteClientDelegate: Send + Sync { fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp); fn get_download_params( &self, - platform: SshPlatform, + platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, ) -> Task>>; - fn download_server_binary_locally( &self, - platform: SshPlatform, + platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, @@ -335,157 +68,6 @@ pub trait SshClientDelegate: Send + Sync { fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp); } -impl SshSocket { - #[cfg(not(target_os = "windows"))] - fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { - Ok(Self { - connection_options: options, - socket_path, - }) - } - - #[cfg(target_os = "windows")] - fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result { - let askpass_script = temp_dir.path().join("askpass.bat"); - std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?; - let mut envs = HashMap::default(); - envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); - envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string()); - envs.insert("ZED_SSH_ASKPASS".into(), secret); - Ok(Self { - connection_options: options, - envs, - }) - } - - // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: - // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l - // and passes -l as an argument to sh, not to ls. - // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing - // into a machine. You must use `cd` to get back to $HOME. - // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" - fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { - let mut command = util::command::new_smol_command("ssh"); - let to_run = iter::once(&program) - .chain(args.iter()) - .map(|token| { - // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? - debug_assert!( - !token.contains('\n'), - "multiline arguments do not work in all shells" - ); - shlex::try_quote(token).unwrap() - }) - .join(" "); - let to_run = format!("cd; {to_run}"); - log::debug!("ssh {} {:?}", self.connection_options.ssh_url(), to_run); - self.ssh_options(&mut command) - .arg(self.connection_options.ssh_url()) - .arg(to_run); - command - } - - async fn run_command(&self, program: &str, args: &[&str]) -> Result { - let output = self.ssh_command(program, args).output().await?; - anyhow::ensure!( - output.status.success(), - "failed to run command: {}", - String::from_utf8_lossy(&output.stderr) - ); - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - #[cfg(not(target_os = "windows"))] - fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(self.connection_options.additional_args()) - .args(["-o", "ControlMaster=no", "-o"]) - .arg(format!("ControlPath={}", self.socket_path.display())) - } - - #[cfg(target_os = "windows")] - fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(self.connection_options.additional_args()) - .envs(self.envs.clone()) - } - - // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. - // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to - #[cfg(not(target_os = "windows"))] - fn ssh_args(&self) -> SshArgs { - let mut arguments = self.connection_options.additional_args(); - arguments.extend(vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ]); - SshArgs { - arguments, - envs: None, - } - } - - #[cfg(target_os = "windows")] - fn ssh_args(&self) -> SshArgs { - let mut arguments = self.connection_options.additional_args(); - arguments.push(self.connection_options.ssh_url()); - SshArgs { - arguments, - envs: Some(self.envs.clone()), - } - } - - async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os.trim() { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(SshPlatform { os, arch }) - } - - async fn shell(&self) -> String { - match self.run_command("sh", &["-c", "echo $SHELL"]).await { - Ok(shell) => shell.trim().to_owned(), - Err(e) => { - log::error!("Failed to get shell: {e}"); - "sh".to_owned() - } - } - } -} - const MAX_MISSED_HEARTBEATS: usize = 5; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); @@ -496,7 +78,7 @@ enum State { Connecting, Connected { ssh_connection: Arc, - delegate: Arc, + delegate: Arc, multiplex_task: Task>, heartbeat_task: Task>, @@ -505,7 +87,7 @@ enum State { missed_heartbeats: usize, ssh_connection: Arc, - delegate: Arc, + delegate: Arc, multiplex_task: Task>, heartbeat_task: Task>, @@ -513,7 +95,7 @@ enum State { Reconnecting, ReconnectFailed { ssh_connection: Arc, - delegate: Arc, + delegate: Arc, error: anyhow::Error, attempts: usize, @@ -537,7 +119,7 @@ impl fmt::Display for State { } impl State { - fn ssh_connection(&self) -> Option<&dyn RemoteConnection> { + fn remote_connection(&self) -> Option<&dyn RemoteConnection> { match self { Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()), Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()), @@ -647,20 +229,20 @@ impl From<&State> for ConnectionState { } } -pub struct SshRemoteClient { +pub struct RemoteClient { client: Arc, unique_identifier: String, connection_options: SshConnectionOptions, path_style: PathStyle, - state: Arc>>, + state: Option, } #[derive(Debug)] -pub enum SshRemoteEvent { +pub enum RemoteClientEvent { Disconnected, } -impl EventEmitter for SshRemoteClient {} +impl EventEmitter for RemoteClient {} // Identifies the socket on the remote server so that reconnects // can re-join the same project. @@ -696,12 +278,12 @@ impl ConnectionIdentifier { } } -impl SshRemoteClient { - pub fn new( +impl RemoteClient { + pub fn ssh( unique_identifier: ConnectionIdentifier, connection_options: SshConnectionOptions, cancellation: oneshot::Receiver<()>, - delegate: Arc, + delegate: Arc, cx: &mut App, ) -> Task>>> { let unique_identifier = unique_identifier.to_string(cx); @@ -729,7 +311,7 @@ impl SshRemoteClient { unique_identifier: unique_identifier.clone(), connection_options, path_style, - state: Arc::new(Mutex::new(Some(State::Connecting))), + state: Some(State::Connecting), })?; let io_task = ssh_connection.start_proxy( @@ -752,7 +334,7 @@ impl SshRemoteClient { let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx); this.update(cx, |this, _| { - *this.state.lock() = Some(State::Connected { + this.state = Some(State::Connected { ssh_connection, delegate, multiplex_task, @@ -782,11 +364,11 @@ impl SshRemoteClient { } pub fn shutdown_processes( - &self, + &mut self, shutdown_request: Option, executor: BackgroundExecutor, ) -> Option + use> { - let state = self.state.lock().take()?; + let state = self.state.take()?; log::info!("shutting down ssh processes"); let State::Connected { @@ -821,15 +403,14 @@ impl SshRemoteClient { } fn reconnect(&mut self, cx: &mut Context) -> Result<()> { - let mut lock = self.state.lock(); - - let can_reconnect = lock + let can_reconnect = self + .state .as_ref() .map(|state| state.can_reconnect()) .unwrap_or(false); if !can_reconnect { log::info!("aborting reconnect, because not in state that allows reconnecting"); - let error = if let Some(state) = lock.as_ref() { + let error = if let Some(state) = self.state.as_ref() { format!("invalid state, cannot reconnect while in state {state}") } else { "no state set".to_string() @@ -837,7 +418,7 @@ impl SshRemoteClient { anyhow::bail!(error); } - let state = lock.take().unwrap(); + let state = self.state.take().unwrap(); let (attempts, ssh_connection, delegate) = match state { State::Connected { ssh_connection, @@ -874,11 +455,9 @@ impl SshRemoteClient { "Failed to reconnect to after {} attempts, giving up", MAX_RECONNECT_ATTEMPTS ); - drop(lock); self.set_state(State::ReconnectExhausted, cx); return Ok(()); } - drop(lock); self.set_state(State::Reconnecting, cx); @@ -1076,7 +655,7 @@ impl SshRemoteClient { missed_heartbeats: usize, cx: &mut Context, ) -> ControlFlow<()> { - let state = self.state.lock().take().unwrap(); + let state = self.state.take().unwrap(); let next_state = if missed_heartbeats > 0 { state.heartbeat_missed() } else { @@ -1139,37 +718,34 @@ impl SshRemoteClient { } fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { - self.state.lock().as_ref().is_some_and(check) + self.state.as_ref().is_some_and(check) } - fn try_set_state(&self, cx: &mut Context, map: impl FnOnce(&State) -> Option) { - let mut lock = self.state.lock(); - let new_state = lock.as_ref().and_then(map); - + fn try_set_state(&mut self, cx: &mut Context, map: impl FnOnce(&State) -> Option) { + let new_state = self.state.as_ref().and_then(map); if let Some(new_state) = new_state { - lock.replace(new_state); + self.state.replace(new_state); cx.notify(); } } - fn set_state(&self, state: State, cx: &mut Context) { + fn set_state(&mut self, state: State, cx: &mut Context) { log::info!("setting state to '{}'", &state); let is_reconnect_exhausted = state.is_reconnect_exhausted(); let is_server_not_running = state.is_server_not_running(); - self.state.lock().replace(state); + self.state.replace(state); if is_reconnect_exhausted || is_server_not_running { - cx.emit(SshRemoteEvent::Disconnected); + cx.emit(RemoteClientEvent::Disconnected); } cx.notify(); } pub fn ssh_info(&self) -> Option { self.state - .lock() .as_ref() - .and_then(|state| state.ssh_connection()) + .and_then(|state| state.remote_connection()) .map(|ssh_connection| SshInfo { args: ssh_connection.ssh_args(), path_style: ssh_connection.path_style(), @@ -1183,8 +759,11 @@ impl SshRemoteClient { dest_path: RemotePathBuf, cx: &App, ) -> Task> { - let state = self.state.lock(); - let Some(connection) = state.as_ref().and_then(|state| state.ssh_connection()) else { + let Some(connection) = self + .state + .as_ref() + .and_then(|state| state.remote_connection()) + else { return Task::ready(Err(anyhow!("no ssh connection"))); }; connection.upload_directory(src_path, dest_path, cx) @@ -1194,17 +773,12 @@ impl SshRemoteClient { self.client.clone().into() } - pub fn connection_string(&self) -> String { - self.connection_options.connection_string() - } - pub fn connection_options(&self) -> SshConnectionOptions { self.connection_options.clone() } pub fn connection_state(&self) -> ConnectionState { self.state - .lock() .as_ref() .map(ConnectionState::from) .unwrap_or(ConnectionState::Disconnected) @@ -1286,7 +860,7 @@ impl SshRemoteClient { let (_tx, rx) = oneshot::channel(); client_cx .update(|cx| { - Self::new( + Self::ssh( ConnectionIdentifier::setup(), opts, rx, @@ -1316,7 +890,7 @@ impl ConnectionPool { pub fn connect( &mut self, opts: SshConnectionOptions, - delegate: &Arc, + delegate: &Arc, cx: &mut App, ) -> Shared, Arc>>> { let connection = self.connections.get(&opts); @@ -1378,14 +952,8 @@ impl ConnectionPool { } } -impl From for AnyProtoClient { - fn from(client: SshRemoteClient) -> Self { - AnyProtoClient::new(client.client) - } -} - #[async_trait(?Send)] -trait RemoteConnection: Send + Sync { +pub(crate) trait RemoteConnection: Send + Sync { fn start_proxy( &self, unique_identifier: String, @@ -1393,7 +961,7 @@ trait RemoteConnection: Send + Sync { incoming_tx: UnboundedSender, outgoing_rx: UnboundedReceiver, connection_activity_tx: Sender<()>, - delegate: Arc, + delegate: Arc, cx: &mut AsyncApp, ) -> Task>; fn upload_directory( @@ -1415,879 +983,6 @@ trait RemoteConnection: Send + Sync { fn simulate_disconnect(&self, _: &AsyncApp) {} } -struct SshRemoteConnection { - socket: SshSocket, - master_process: Mutex>, - remote_binary_path: Option, - ssh_platform: SshPlatform, - ssh_path_style: PathStyle, - ssh_shell: String, - _temp_dir: TempDir, -} - -#[async_trait(?Send)] -impl RemoteConnection for SshRemoteConnection { - async fn kill(&self) -> Result<()> { - let Some(mut process) = self.master_process.lock().take() else { - return Ok(()); - }; - process.kill().ok(); - process.status().await?; - Ok(()) - } - - fn has_been_killed(&self) -> bool { - self.master_process.lock().is_none() - } - - fn ssh_args(&self) -> SshArgs { - self.socket.ssh_args() - } - - fn connection_options(&self) -> SshConnectionOptions { - self.socket.connection_options.clone() - } - - fn shell(&self) -> String { - self.ssh_shell.clone() - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg("-C") - .arg("-r") - .arg(&src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path - )) - .output(); - - cx.background_spawn(async move { - let output = output.await?; - - anyhow::ensure!( - output.status.success(), - "failed to upload directory {} -> {}: {}", - src_path.display(), - dest_path.to_string(), - String::from_utf8_lossy(&output.stderr) - ); - - Ok(()) - }) - } - - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_path) = self.remote_binary_path.clone() else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut start_proxy_command = shell_script!( - "exec {binary_path} proxy --identifier {identifier}", - binary_path = &remote_binary_path.to_string(), - identifier = &unique_identifier, - ); - - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - start_proxy_command = format!( - "{}={} {} ", - env_var, - shlex::try_quote(&value).unwrap(), - start_proxy_command, - ); - } - } - - if reconnect { - start_proxy_command.push_str(" --reconnect"); - } - - let ssh_proxy_process = match self - .socket - .ssh_command("sh", &["-c", &start_proxy_command]) - // IMPORTANT: we kill this process when we drop the task that uses it. - .kill_on_drop(true) - .spawn() - { - Ok(process) => process, - Err(error) => { - return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); - } - }; - - Self::multiplex( - ssh_proxy_process, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn path_style(&self) -> PathStyle { - self.ssh_path_style - } -} - -impl SshRemoteConnection { - async fn new( - connection_options: SshConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - use askpass::AskPassResult; - - delegate.set_status(Some("Connecting"), cx); - - let url = connection_options.ssh_url(); - - let temp_dir = tempfile::Builder::new() - .prefix("zed-ssh-session") - .tempdir()?; - let askpass_delegate = askpass::AskPassDelegate::new(cx, { - let delegate = delegate.clone(); - move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) - }); - - let mut askpass = - askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; - - // Start the master SSH process, which does not do anything except for establish - // the connection and keep it open, allowing other ssh commands to reuse it - // via a control socket. - #[cfg(not(target_os = "windows"))] - let socket_path = temp_dir.path().join("ssh.sock"); - - let mut master_process = { - #[cfg(not(target_os = "windows"))] - let args = [ - "-N", - "-o", - "ControlPersist=no", - "-o", - "ControlMaster=yes", - "-o", - ]; - // On Windows, `ControlMaster` and `ControlPath` are not supported: - // https://github.com/PowerShell/Win32-OpenSSH/issues/405 - // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope - #[cfg(target_os = "windows")] - let args = ["-N"]; - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass.script_path()) - .args(connection_options.additional_args()) - .args(args); - #[cfg(not(target_os = "windows"))] - master_process.arg(format!("ControlPath={}", socket_path.display())); - master_process.arg(&url).spawn()? - }; - // Wait for this ssh process to close its stdout, indicating that authentication - // has completed. - let mut stdout = master_process.stdout.take().unwrap(); - let mut output = Vec::new(); - - let result = select_biased! { - result = askpass.run().fuse() => { - match result { - AskPassResult::CancelledByUser => { - master_process.kill().ok(); - anyhow::bail!("SSH connection canceled") - } - AskPassResult::Timedout => { - anyhow::bail!("connecting to host timed out") - } - } - } - _ = stdout.read_to_end(&mut output).fuse() => { - anyhow::Ok(()) - } - }; - - if let Err(e) = result { - return Err(e.context("Failed to connect to host")); - } - - if master_process.try_status()?.is_some() { - output.clear(); - let mut stderr = master_process.stderr.take().unwrap(); - stderr.read_to_end(&mut output).await?; - - let error_message = format!( - "failed to connect: {}", - String::from_utf8_lossy(&output).trim() - ); - anyhow::bail!(error_message); - } - - #[cfg(not(target_os = "windows"))] - let socket = SshSocket::new(connection_options, socket_path)?; - #[cfg(target_os = "windows")] - let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?; - drop(askpass); - - let ssh_platform = socket.platform().await?; - let ssh_path_style = match ssh_platform.os { - "windows" => PathStyle::Windows, - _ => PathStyle::Posix, - }; - let ssh_shell = socket.shell().await; - - let mut this = Self { - socket, - master_process: Mutex::new(Some(master_process)), - _temp_dir: temp_dir, - remote_binary_path: None, - ssh_path_style, - ssh_platform, - ssh_shell, - }; - - let (release_channel, version, commit) = cx.update(|cx| { - ( - ReleaseChannel::global(cx), - AppVersion::global(cx), - AppCommitSha::try_global(cx), - ) - })?; - this.remote_binary_path = Some( - this.ensure_server_binary(&delegate, release_channel, version, commit, cx) - .await?, - ); - - Ok(this) - } - - fn multiplex( - mut ssh_proxy_process: Child, - incoming_tx: UnboundedSender, - mut outgoing_rx: UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - cx: &AsyncApp, - ) -> Task> { - let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); - let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); - let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); - - let mut stdin_buffer = Vec::new(); - let mut stdout_buffer = Vec::new(); - let mut stderr_buffer = Vec::new(); - let mut stderr_offset = 0; - - let stdin_task = cx.background_spawn(async move { - while let Some(outgoing) = outgoing_rx.next().await { - write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; - } - anyhow::Ok(()) - }); - - let stdout_task = cx.background_spawn({ - let mut connection_activity_tx = connection_activity_tx.clone(); - async move { - loop { - stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); - let len = child_stdout.read(&mut stdout_buffer).await?; - - if len == 0 { - return anyhow::Ok(()); - } - - if len < MESSAGE_LEN_SIZE { - child_stdout.read_exact(&mut stdout_buffer[len..]).await?; - } - - let message_len = message_len_from_buffer(&stdout_buffer); - let envelope = - read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) - .await?; - connection_activity_tx.try_send(()).ok(); - incoming_tx.unbounded_send(envelope).ok(); - } - } - }); - - let stderr_task: Task> = cx.background_spawn(async move { - loop { - stderr_buffer.resize(stderr_offset + 1024, 0); - - let len = child_stderr - .read(&mut stderr_buffer[stderr_offset..]) - .await?; - if len == 0 { - return anyhow::Ok(()); - } - - stderr_offset += len; - let mut start_ix = 0; - while let Some(ix) = stderr_buffer[start_ix..stderr_offset] - .iter() - .position(|b| b == &b'\n') - { - let line_ix = start_ix + ix; - let content = &stderr_buffer[start_ix..line_ix]; - start_ix = line_ix + 1; - if let Ok(record) = serde_json::from_slice::(content) { - record.log(log::logger()) - } else { - eprintln!("(remote) {}", String::from_utf8_lossy(content)); - } - } - stderr_buffer.drain(0..start_ix); - stderr_offset -= start_ix; - - connection_activity_tx.try_send(()).ok(); - } - }); - - cx.background_spawn(async move { - let result = futures::select! { - result = stdin_task.fuse() => { - result.context("stdin") - } - result = stdout_task.fuse() => { - result.context("stdout") - } - result = stderr_task.fuse() => { - result.context("stderr") - } - }; - - let status = ssh_proxy_process.status().await?.code().unwrap_or(1); - match result { - Ok(_) => Ok(status), - Err(error) => Err(error), - } - }) - } - - #[allow(unused)] - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: SemanticVersion, - commit: Option, - cx: &mut AsyncApp, - ) -> Result { - let version_str = match release_channel { - ReleaseChannel::Nightly => { - let commit = commit.map(|s| s.full()).unwrap_or_default(); - format!("{}-{}", version, commit) - } - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - let dst_path = RemotePathBuf::new( - paths::remote_server_dir_relative().join(binary_name), - 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?; - let tmp_path = RemotePathBuf::new( - paths::remote_server_dir_relative().join(format!( - "download-{}-{}", - std::process::id(), - src_path.file_name().unwrap().to_string_lossy() - )), - self.ssh_path_style, - ); - self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) - .await?; - self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .socket - .run_command(&dst_path.to_string(), &["version"]) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = cx.update(|cx| match release_channel { - ReleaseChannel::Nightly => Ok(None), - ReleaseChannel::Dev => { - anyhow::bail!( - "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", - dst_path - ) - } - _ => Ok(Some(AppVersion::global(cx))), - })??; - - let tmp_path_gz = RemotePathBuf::new( - PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())), - self.ssh_path_style, - ); - if !self.socket.connection_options.upload_binary_over_ssh - && let Some((url, body)) = delegate - .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) - .await? - { - match self - .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to upload server: {}", - e - ) - } - } - } - - let src_path = delegate - .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx) - .await?; - self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) - .await?; - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await?; - Ok(dst_path) - } - - async fn download_binary_on_server( - &self, - url: &str, - body: &str, - tmp_path_gz: &RemotePathBuf, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - "sh", - &[ - "-c", - &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), - ], - ) - .await?; - } - - delegate.set_status(Some("Downloading remote development server on host"), cx); - - match self - .socket - .run_command( - "curl", - &[ - "-f", - "-L", - "-X", - "GET", - "-H", - "Content-Type: application/json", - "-d", - body, - url, - "-o", - &tmp_path_gz.to_string(), - ], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self.socket.run_command("which", &["curl"]).await.is_ok() { - return Err(e); - } - - match self - .socket - .run_command( - "wget", - &[ - "--method=GET", - "--header=Content-Type: application/json", - "--body-data", - body, - url, - "-O", - &tmp_path_gz.to_string(), - ], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self.socket.run_command("which", &["wget"]).await.is_ok() { - return Err(e); - } else { - anyhow::bail!("Neither curl nor wget is available"); - } - } - } - } - } - - Ok(()) - } - - async fn upload_local_server_binary( - &self, - src_path: &Path, - tmp_path_gz: &RemotePathBuf, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - "sh", - &[ - "-c", - &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), - ], - ) - .await?; - } - - let src_stat = fs::metadata(&src_path).await?; - let size = src_stat.len(); - - let t0 = Instant::now(); - delegate.set_status(Some("Uploading remote development server"), cx); - log::info!( - "uploading remote development server to {:?} ({}kb)", - tmp_path_gz, - size / 1024 - ); - self.upload_file(src_path, tmp_path_gz) - .await - .context("failed to upload server binary")?; - log::info!("uploaded remote development server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn extract_server_binary( - &self, - dst_path: &RemotePathBuf, - tmp_path: &RemotePathBuf, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote development server"), cx); - let server_mode = 0o755; - - let orig_tmp_path = tmp_path.to_string(); - let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - shell_script!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.to_string(), - ) - } else { - shell_script!( - "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", - server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.to_string() - ) - }; - self.socket.run_command("sh", &["-c", &script]).await?; - Ok(()) - } - - async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg(src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path - )) - .output() - .await?; - - anyhow::ensure!( - output.status.success(), - "failed to upload file {} -> {}: {}", - src_path.display(), - dest_path.to_string(), - String::from_utf8_lossy(&output.stderr) - ); - Ok(()) - } - - #[cfg(debug_assertions)] - async fn build_local( - &self, - build_remote_server: String, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result { - use smol::process::{Command, Stdio}; - use std::env::VarError; - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - anyhow::ensure!( - output.status.success(), - "Failed to run command: {command:?}" - ); - Ok(()) - } - - let use_musl = !build_remote_server.contains("nomusl"); - let triple = format!( - "{}-{}", - self.ssh_platform.arch, - match self.ssh_platform.os { - "linux" => - if use_musl { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), - } - ); - let mut rust_flags = match std::env::var("RUSTFLAGS") { - Ok(val) => val, - Err(VarError::NotPresent) => String::new(), - Err(e) => { - log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); - String::new() - } - }; - if self.ssh_platform.os == "linux" && use_musl { - rust_flags.push_str(" -C target-feature=+crt-static"); - } - if build_remote_server.contains("mold") { - rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); - } - - if self.ssh_platform.arch == std::env::consts::ARCH - && self.ssh_platform.os == std::env::consts::OS - { - delegate.set_status(Some("Building remote server binary from source"), cx); - log::info!("building remote server binary from source"); - run_cmd( - Command::new("cargo") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; - - delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; - - delegate.set_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); - - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::from(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) - } -} - type ResponseChannels = Mutex)>>>; struct ChannelClient { @@ -2501,7 +1196,7 @@ impl ChannelClient { .await } - pub fn send(&self, payload: T) -> Result<()> { + fn send(&self, payload: T) -> Result<()> { log::debug!("ssh send name:{}", T::NAME); self.send_dynamic(payload.into_envelope(0, None, None)) } @@ -2586,8 +1281,8 @@ impl ProtoClient for ChannelClient { #[cfg(any(test, feature = "test-support"))] mod fake { - use std::{path::PathBuf, sync::Arc}; - + use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform, SshArgs}; + use crate::SshConnectionOptions; use anyhow::Result; use async_trait::async_trait; use futures::{ @@ -2601,13 +1296,9 @@ mod fake { use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext}; use release_channel::ReleaseChannel; use rpc::proto::Envelope; + use std::{path::PathBuf, sync::Arc}; use util::paths::{PathStyle, RemotePathBuf}; - use super::{ - ChannelClient, RemoteConnection, SshArgs, SshClientDelegate, SshConnectionOptions, - SshPlatform, - }; - pub(super) struct FakeRemoteConnection { pub(super) connection_options: SshConnectionOptions, pub(super) server_channel: Arc, @@ -2675,7 +1366,7 @@ mod fake { mut client_incoming_tx: mpsc::UnboundedSender, mut client_outgoing_rx: mpsc::UnboundedReceiver, mut connection_activity_tx: Sender<()>, - _delegate: Arc, + _delegate: Arc, cx: &mut AsyncApp, ) -> Task> { let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); @@ -2719,14 +1410,14 @@ mod fake { pub(super) struct Delegate; - impl SshClientDelegate for Delegate { + impl RemoteClientDelegate for Delegate { fn ask_password(&self, _: String, _: oneshot::Sender, _: &mut AsyncApp) { unreachable!() } fn download_server_binary_locally( &self, - _: SshPlatform, + _: RemotePlatform, _: ReleaseChannel, _: Option, _: &mut AsyncApp, @@ -2736,7 +1427,7 @@ mod fake { fn get_download_params( &self, - _platform: SshPlatform, + _platform: RemotePlatform, _release_channel: ReleaseChannel, _version: Option, _cx: &mut AsyncApp, diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs new file mode 100644 index 0000000000..aa086fd3f5 --- /dev/null +++ b/crates/remote/src/transport.rs @@ -0,0 +1 @@ +pub mod ssh; diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs new file mode 100644 index 0000000000..5b026b1111 --- /dev/null +++ b/crates/remote/src/transport/ssh.rs @@ -0,0 +1,1313 @@ +use crate::{ + RemoteClientDelegate, RemotePlatform, + json_log::LogRecord, + protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, + remote_client::RemoteConnection, +}; +use anyhow::{Context as _, Result, anyhow}; +use async_trait::async_trait; +use collections::HashMap; +use futures::{ + AsyncReadExt as _, FutureExt as _, StreamExt as _, + channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, + select_biased, +}; +use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task}; +use itertools::Itertools; +use parking_lot::Mutex; +use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use rpc::proto::Envelope; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use smol::{ + fs, + process::{self, Child, Stdio}, +}; +use std::{ + iter, + path::{Path, PathBuf}, + sync::Arc, + time::Instant, +}; +use tempfile::TempDir; +use util::paths::{PathStyle, RemotePathBuf}; + +pub(crate) struct SshRemoteConnection { + socket: SshSocket, + master_process: Mutex>, + remote_binary_path: Option, + ssh_platform: RemotePlatform, + ssh_path_style: PathStyle, + ssh_shell: String, + _temp_dir: TempDir, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct SshConnectionOptions { + pub host: String, + pub username: Option, + pub port: Option, + pub password: Option, + pub args: Option>, + pub port_forwards: Option>, + + pub nickname: Option, + pub upload_binary_over_ssh: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] +pub struct SshPortForwardOption { + #[serde(skip_serializing_if = "Option::is_none")] + pub local_host: Option, + pub local_port: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_host: Option, + pub remote_port: u16, +} + +#[derive(Clone)] +struct SshSocket { + connection_options: SshConnectionOptions, + #[cfg(not(target_os = "windows"))] + socket_path: PathBuf, + #[cfg(target_os = "windows")] + envs: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshArgs { + pub arguments: Vec, + pub envs: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshInfo { + pub args: SshArgs, + pub path_style: PathStyle, + pub shell: String, +} + +macro_rules! shell_script { + ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ + format!( + $fmt, + $( + $name = shlex::try_quote($arg).unwrap() + ),+ + ) + }}; +} + +#[async_trait(?Send)] +impl RemoteConnection for SshRemoteConnection { + async fn kill(&self) -> Result<()> { + let Some(mut process) = self.master_process.lock().take() else { + return Ok(()); + }; + process.kill().ok(); + process.status().await?; + Ok(()) + } + + fn has_been_killed(&self) -> bool { + self.master_process.lock().is_none() + } + + fn ssh_args(&self) -> SshArgs { + self.socket.ssh_args() + } + + fn connection_options(&self) -> SshConnectionOptions { + self.socket.connection_options.clone() + } + + fn shell(&self) -> String { + self.ssh_shell.clone() + } + + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task> { + let mut command = util::command::new_smol_command("scp"); + let output = self + .socket + .ssh_options(&mut command) + .args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ) + .arg("-C") + .arg("-r") + .arg(&src_path) + .arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path + )) + .output(); + + cx.background_spawn(async move { + let output = output.await?; + + anyhow::ensure!( + output.status.success(), + "failed to upload directory {} -> {}: {}", + src_path.display(), + dest_path.to_string(), + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + }) + } + + fn start_proxy( + &self, + unique_identifier: String, + reconnect: bool, + incoming_tx: UnboundedSender, + outgoing_rx: UnboundedReceiver, + connection_activity_tx: Sender<()>, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Task> { + delegate.set_status(Some("Starting proxy"), cx); + + let Some(remote_binary_path) = self.remote_binary_path.clone() else { + return Task::ready(Err(anyhow!("Remote binary path not set"))); + }; + + let mut start_proxy_command = shell_script!( + "exec {binary_path} proxy --identifier {identifier}", + binary_path = &remote_binary_path.to_string(), + identifier = &unique_identifier, + ); + + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + start_proxy_command = format!( + "{}={} {} ", + env_var, + shlex::try_quote(&value).unwrap(), + start_proxy_command, + ); + } + } + + if reconnect { + start_proxy_command.push_str(" --reconnect"); + } + + let ssh_proxy_process = match self + .socket + .ssh_command("sh", &["-c", &start_proxy_command]) + // IMPORTANT: we kill this process when we drop the task that uses it. + .kill_on_drop(true) + .spawn() + { + Ok(process) => process, + Err(error) => { + return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); + } + }; + + Self::multiplex( + ssh_proxy_process, + incoming_tx, + outgoing_rx, + connection_activity_tx, + cx, + ) + } + + fn path_style(&self) -> PathStyle { + self.ssh_path_style + } +} + +impl SshRemoteConnection { + pub(crate) async fn new( + connection_options: SshConnectionOptions, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Result { + use askpass::AskPassResult; + + delegate.set_status(Some("Connecting"), cx); + + let url = connection_options.ssh_url(); + + let temp_dir = tempfile::Builder::new() + .prefix("zed-ssh-session") + .tempdir()?; + let askpass_delegate = askpass::AskPassDelegate::new(cx, { + let delegate = delegate.clone(); + move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) + }); + + let mut askpass = + askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; + + // Start the master SSH process, which does not do anything except for establish + // the connection and keep it open, allowing other ssh commands to reuse it + // via a control socket. + #[cfg(not(target_os = "windows"))] + let socket_path = temp_dir.path().join("ssh.sock"); + + let mut master_process = { + #[cfg(not(target_os = "windows"))] + let args = [ + "-N", + "-o", + "ControlPersist=no", + "-o", + "ControlMaster=yes", + "-o", + ]; + // On Windows, `ControlMaster` and `ControlPath` are not supported: + // https://github.com/PowerShell/Win32-OpenSSH/issues/405 + // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope + #[cfg(target_os = "windows")] + let args = ["-N"]; + let mut master_process = util::command::new_smol_command("ssh"); + master_process + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("SSH_ASKPASS_REQUIRE", "force") + .env("SSH_ASKPASS", askpass.script_path()) + .args(connection_options.additional_args()) + .args(args); + #[cfg(not(target_os = "windows"))] + master_process.arg(format!("ControlPath={}", socket_path.display())); + master_process.arg(&url).spawn()? + }; + // Wait for this ssh process to close its stdout, indicating that authentication + // has completed. + let mut stdout = master_process.stdout.take().unwrap(); + let mut output = Vec::new(); + + let result = select_biased! { + result = askpass.run().fuse() => { + match result { + AskPassResult::CancelledByUser => { + master_process.kill().ok(); + anyhow::bail!("SSH connection canceled") + } + AskPassResult::Timedout => { + anyhow::bail!("connecting to host timed out") + } + } + } + _ = stdout.read_to_end(&mut output).fuse() => { + anyhow::Ok(()) + } + }; + + if let Err(e) = result { + return Err(e.context("Failed to connect to host")); + } + + if master_process.try_status()?.is_some() { + output.clear(); + let mut stderr = master_process.stderr.take().unwrap(); + stderr.read_to_end(&mut output).await?; + + let error_message = format!( + "failed to connect: {}", + String::from_utf8_lossy(&output).trim() + ); + anyhow::bail!(error_message); + } + + #[cfg(not(target_os = "windows"))] + let socket = SshSocket::new(connection_options, socket_path)?; + #[cfg(target_os = "windows")] + let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?; + drop(askpass); + + let ssh_platform = socket.platform().await?; + let ssh_path_style = match ssh_platform.os { + "windows" => PathStyle::Windows, + _ => PathStyle::Posix, + }; + let ssh_shell = socket.shell().await; + + let mut this = Self { + socket, + master_process: Mutex::new(Some(master_process)), + _temp_dir: temp_dir, + remote_binary_path: None, + ssh_path_style, + ssh_platform, + ssh_shell, + }; + + let (release_channel, version, commit) = cx.update(|cx| { + ( + ReleaseChannel::global(cx), + AppVersion::global(cx), + AppCommitSha::try_global(cx), + ) + })?; + this.remote_binary_path = Some( + this.ensure_server_binary(&delegate, release_channel, version, commit, cx) + .await?, + ); + + Ok(this) + } + + fn multiplex( + mut ssh_proxy_process: Child, + incoming_tx: UnboundedSender, + mut outgoing_rx: UnboundedReceiver, + mut connection_activity_tx: Sender<()>, + cx: &AsyncApp, + ) -> Task> { + let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); + let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); + let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); + + let mut stdin_buffer = Vec::new(); + let mut stdout_buffer = Vec::new(); + let mut stderr_buffer = Vec::new(); + let mut stderr_offset = 0; + + let stdin_task = cx.background_spawn(async move { + while let Some(outgoing) = outgoing_rx.next().await { + write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; + } + anyhow::Ok(()) + }); + + let stdout_task = cx.background_spawn({ + let mut connection_activity_tx = connection_activity_tx.clone(); + async move { + loop { + stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); + let len = child_stdout.read(&mut stdout_buffer).await?; + + if len == 0 { + return anyhow::Ok(()); + } + + if len < MESSAGE_LEN_SIZE { + child_stdout.read_exact(&mut stdout_buffer[len..]).await?; + } + + let message_len = message_len_from_buffer(&stdout_buffer); + let envelope = + read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) + .await?; + connection_activity_tx.try_send(()).ok(); + incoming_tx.unbounded_send(envelope).ok(); + } + } + }); + + let stderr_task: Task> = cx.background_spawn(async move { + loop { + stderr_buffer.resize(stderr_offset + 1024, 0); + + let len = child_stderr + .read(&mut stderr_buffer[stderr_offset..]) + .await?; + if len == 0 { + return anyhow::Ok(()); + } + + stderr_offset += len; + let mut start_ix = 0; + while let Some(ix) = stderr_buffer[start_ix..stderr_offset] + .iter() + .position(|b| b == &b'\n') + { + let line_ix = start_ix + ix; + let content = &stderr_buffer[start_ix..line_ix]; + start_ix = line_ix + 1; + if let Ok(record) = serde_json::from_slice::(content) { + record.log(log::logger()) + } else { + eprintln!("(remote) {}", String::from_utf8_lossy(content)); + } + } + stderr_buffer.drain(0..start_ix); + stderr_offset -= start_ix; + + connection_activity_tx.try_send(()).ok(); + } + }); + + cx.background_spawn(async move { + let result = futures::select! { + result = stdin_task.fuse() => { + result.context("stdin") + } + result = stdout_task.fuse() => { + result.context("stdout") + } + result = stderr_task.fuse() => { + result.context("stderr") + } + }; + + let status = ssh_proxy_process.status().await?.code().unwrap_or(1); + match result { + Ok(_) => Ok(status), + Err(error) => Err(error), + } + }) + } + + #[allow(unused)] + async fn ensure_server_binary( + &self, + delegate: &Arc, + release_channel: ReleaseChannel, + version: SemanticVersion, + commit: Option, + cx: &mut AsyncApp, + ) -> Result { + let version_str = match release_channel { + ReleaseChannel::Nightly => { + let commit = commit.map(|s| s.full()).unwrap_or_default(); + format!("{}-{}", version, commit) + } + ReleaseChannel::Dev => "build".to_string(), + _ => version.to_string(), + }; + let binary_name = format!( + "zed-remote-server-{}-{}", + release_channel.dev_name(), + version_str + ); + let dst_path = RemotePathBuf::new( + paths::remote_server_dir_relative().join(binary_name), + 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?; + let tmp_path = RemotePathBuf::new( + paths::remote_server_dir_relative().join(format!( + "download-{}-{}", + std::process::id(), + src_path.file_name().unwrap().to_string_lossy() + )), + self.ssh_path_style, + ); + self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) + .await?; + self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) + .await?; + return Ok(dst_path); + } + + if self + .socket + .run_command(&dst_path.to_string(), &["version"]) + .await + .is_ok() + { + return Ok(dst_path); + } + + let wanted_version = cx.update(|cx| match release_channel { + ReleaseChannel::Nightly => Ok(None), + ReleaseChannel::Dev => { + anyhow::bail!( + "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", + dst_path + ) + } + _ => Ok(Some(AppVersion::global(cx))), + })??; + + let tmp_path_gz = RemotePathBuf::new( + PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())), + self.ssh_path_style, + ); + if !self.socket.connection_options.upload_binary_over_ssh + && let Some((url, body)) = delegate + .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) + .await? + { + match self + .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) + .await + { + Ok(_) => { + self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) + .await?; + return Ok(dst_path); + } + Err(e) => { + log::error!( + "Failed to download binary on server, attempting to upload server: {}", + e + ) + } + } + } + + let src_path = delegate + .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx) + .await?; + self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) + .await?; + self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) + .await?; + Ok(dst_path) + } + + async fn download_binary_on_server( + &self, + url: &str, + body: &str, + tmp_path_gz: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + if let Some(parent) = tmp_path_gz.parent() { + self.socket + .run_command( + "sh", + &[ + "-c", + &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), + ], + ) + .await?; + } + + delegate.set_status(Some("Downloading remote development server on host"), cx); + + match self + .socket + .run_command( + "curl", + &[ + "-f", + "-L", + "-X", + "GET", + "-H", + "Content-Type: application/json", + "-d", + body, + url, + "-o", + &tmp_path_gz.to_string(), + ], + ) + .await + { + Ok(_) => {} + Err(e) => { + if self.socket.run_command("which", &["curl"]).await.is_ok() { + return Err(e); + } + + match self + .socket + .run_command( + "wget", + &[ + "--method=GET", + "--header=Content-Type: application/json", + "--body-data", + body, + url, + "-O", + &tmp_path_gz.to_string(), + ], + ) + .await + { + Ok(_) => {} + Err(e) => { + if self.socket.run_command("which", &["wget"]).await.is_ok() { + return Err(e); + } else { + anyhow::bail!("Neither curl nor wget is available"); + } + } + } + } + } + + Ok(()) + } + + async fn upload_local_server_binary( + &self, + src_path: &Path, + tmp_path_gz: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + if let Some(parent) = tmp_path_gz.parent() { + self.socket + .run_command( + "sh", + &[ + "-c", + &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), + ], + ) + .await?; + } + + let src_stat = fs::metadata(&src_path).await?; + let size = src_stat.len(); + + let t0 = Instant::now(); + delegate.set_status(Some("Uploading remote development server"), cx); + log::info!( + "uploading remote development server to {:?} ({}kb)", + tmp_path_gz, + size / 1024 + ); + self.upload_file(src_path, tmp_path_gz) + .await + .context("failed to upload server binary")?; + log::info!("uploaded remote development server in {:?}", t0.elapsed()); + Ok(()) + } + + async fn extract_server_binary( + &self, + dst_path: &RemotePathBuf, + tmp_path: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + delegate.set_status(Some("Extracting remote development server"), cx); + let server_mode = 0o755; + + let orig_tmp_path = tmp_path.to_string(); + let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { + shell_script!( + "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", + server_mode = &format!("{:o}", server_mode), + dst_path = &dst_path.to_string(), + ) + } else { + shell_script!( + "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", + server_mode = &format!("{:o}", server_mode), + dst_path = &dst_path.to_string() + ) + }; + self.socket.run_command("sh", &["-c", &script]).await?; + Ok(()) + } + + async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> { + log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + let mut command = util::command::new_smol_command("scp"); + let output = self + .socket + .ssh_options(&mut command) + .args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ) + .arg(src_path) + .arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path + )) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to upload file {} -> {}: {}", + src_path.display(), + dest_path.to_string(), + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) + } + + #[cfg(debug_assertions)] + async fn build_local( + &self, + build_remote_server: String, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result { + use smol::process::{Command, Stdio}; + use std::env::VarError; + + async fn run_cmd(command: &mut Command) -> Result<()> { + let output = command + .kill_on_drop(true) + .stderr(Stdio::inherit()) + .output() + .await?; + anyhow::ensure!( + output.status.success(), + "Failed to run command: {command:?}" + ); + Ok(()) + } + + let use_musl = !build_remote_server.contains("nomusl"); + let triple = format!( + "{}-{}", + self.ssh_platform.arch, + match self.ssh_platform.os { + "linux" => + if use_musl { + "unknown-linux-musl" + } else { + "unknown-linux-gnu" + }, + "macos" => "apple-darwin", + _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), + } + ); + let mut rust_flags = match std::env::var("RUSTFLAGS") { + Ok(val) => val, + Err(VarError::NotPresent) => String::new(), + Err(e) => { + log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); + String::new() + } + }; + if self.ssh_platform.os == "linux" && use_musl { + rust_flags.push_str(" -C target-feature=+crt-static"); + } + if build_remote_server.contains("mold") { + rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); + } + + if self.ssh_platform.arch == std::env::consts::ARCH + && self.ssh_platform.os == std::env::consts::OS + { + delegate.set_status(Some("Building remote server binary from source"), cx); + log::info!("building remote server binary from source"); + run_cmd( + Command::new("cargo") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; + + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); + + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = + SanitizedPath::from(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 { + #[cfg(not(target_os = "windows"))] + fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { + Ok(Self { + connection_options: options, + socket_path, + }) + } + + #[cfg(target_os = "windows")] + fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result { + let askpass_script = temp_dir.path().join("askpass.bat"); + std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?; + let mut envs = HashMap::default(); + envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); + envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string()); + envs.insert("ZED_SSH_ASKPASS".into(), secret); + Ok(Self { + connection_options: options, + envs, + }) + } + + // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: + // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l + // and passes -l as an argument to sh, not to ls. + // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing + // into a machine. You must use `cd` to get back to $HOME. + // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" + fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { + let mut command = util::command::new_smol_command("ssh"); + let to_run = iter::once(&program) + .chain(args.iter()) + .map(|token| { + // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? + debug_assert!( + !token.contains('\n'), + "multiline arguments do not work in all shells" + ); + shlex::try_quote(token).unwrap() + }) + .join(" "); + let to_run = format!("cd; {to_run}"); + log::debug!("ssh {} {:?}", self.connection_options.ssh_url(), to_run); + self.ssh_options(&mut command) + .arg(self.connection_options.ssh_url()) + .arg(to_run); + command + } + + async fn run_command(&self, program: &str, args: &[&str]) -> Result { + let output = self.ssh_command(program, args).output().await?; + anyhow::ensure!( + output.status.success(), + "failed to run command: {}", + String::from_utf8_lossy(&output.stderr) + ); + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } + + #[cfg(not(target_os = "windows"))] + fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) + .args(["-o", "ControlMaster=no", "-o"]) + .arg(format!("ControlPath={}", self.socket_path.display())) + } + + #[cfg(target_os = "windows")] + fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) + .envs(self.envs.clone()) + } + + // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. + // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to + #[cfg(not(target_os = "windows"))] + fn ssh_args(&self) -> SshArgs { + let mut arguments = self.connection_options.additional_args(); + arguments.extend(vec![ + "-o".to_string(), + "ControlMaster=no".to_string(), + "-o".to_string(), + format!("ControlPath={}", self.socket_path.display()), + self.connection_options.ssh_url(), + ]); + SshArgs { + arguments, + envs: None, + } + } + + #[cfg(target_os = "windows")] + fn ssh_args(&self) -> SshArgs { + let mut arguments = self.connection_options.additional_args(); + arguments.push(self.connection_options.ssh_url()); + SshArgs { + arguments, + envs: Some(self.envs.clone()), + } + } + + async fn platform(&self) -> Result { + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; + let Some((os, arch)) = uname.split_once(" ") else { + anyhow::bail!("unknown uname: {uname:?}") + }; + + let os = match os.trim() { + "Darwin" => "macos", + "Linux" => "linux", + _ => anyhow::bail!( + "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" + ), + }; + // exclude armv5,6,7 as they are 32-bit. + let arch = if arch.starts_with("armv8") + || arch.starts_with("armv9") + || arch.starts_with("arm64") + || arch.starts_with("aarch64") + { + "aarch64" + } else if arch.starts_with("x86") { + "x86_64" + } else { + anyhow::bail!( + "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" + ) + }; + + Ok(RemotePlatform { os, arch }) + } + + async fn shell(&self) -> String { + match self.run_command("sh", &["-c", "echo $SHELL"]).await { + Ok(shell) => shell.trim().to_owned(), + Err(e) => { + log::error!("Failed to get shell: {e}"); + "sh".to_owned() + } + } + } +} + +fn parse_port_number(port_str: &str) -> Result { + port_str + .parse() + .with_context(|| format!("parsing port number: {port_str}")) +} + +fn parse_port_forward_spec(spec: &str) -> Result { + let parts: Vec<&str> = spec.split(':').collect(); + + match parts.len() { + 4 => { + let local_port = parse_port_number(parts[1])?; + let remote_port = parse_port_number(parts[3])?; + + Ok(SshPortForwardOption { + local_host: Some(parts[0].to_string()), + local_port, + remote_host: Some(parts[2].to_string()), + remote_port, + }) + } + 3 => { + let local_port = parse_port_number(parts[0])?; + let remote_port = parse_port_number(parts[2])?; + + Ok(SshPortForwardOption { + local_host: None, + local_port, + remote_host: Some(parts[1].to_string()), + remote_port, + }) + } + _ => anyhow::bail!("Invalid port forward format"), + } +} + +impl SshConnectionOptions { + pub fn parse_command_line(input: &str) -> Result { + let input = input.trim_start_matches("ssh "); + let mut hostname: Option = None; + let mut username: Option = None; + let mut port: Option = None; + let mut args = Vec::new(); + let mut port_forwards: Vec = Vec::new(); + + // disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W + const ALLOWED_OPTS: &[&str] = &[ + "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", + ]; + const ALLOWED_ARGS: &[&str] = &[ + "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", + "-w", + ]; + + let mut tokens = shlex::split(input).context("invalid input")?.into_iter(); + + 'outer: while let Some(arg) = tokens.next() { + if ALLOWED_OPTS.contains(&(&arg as &str)) { + args.push(arg.to_string()); + continue; + } + if arg == "-p" { + port = tokens.next().and_then(|arg| arg.parse().ok()); + continue; + } else if let Some(p) = arg.strip_prefix("-p") { + port = p.parse().ok(); + continue; + } + if arg == "-l" { + username = tokens.next(); + continue; + } else if let Some(l) = arg.strip_prefix("-l") { + username = Some(l.to_string()); + continue; + } + if arg == "-L" || arg.starts_with("-L") { + let forward_spec = if arg == "-L" { + tokens.next() + } else { + Some(arg.strip_prefix("-L").unwrap().to_string()) + }; + + if let Some(spec) = forward_spec { + port_forwards.push(parse_port_forward_spec(&spec)?); + } else { + anyhow::bail!("Missing port forward format"); + } + } + + for a in ALLOWED_ARGS { + if arg == *a { + args.push(arg); + if let Some(next) = tokens.next() { + args.push(next); + } + continue 'outer; + } else if arg.starts_with(a) { + args.push(arg); + continue 'outer; + } + } + if arg.starts_with("-") || hostname.is_some() { + anyhow::bail!("unsupported argument: {:?}", arg); + } + let mut input = &arg as &str; + // Destination might be: username1@username2@ip2@ip1 + if let Some((u, rest)) = input.rsplit_once('@') { + input = rest; + username = Some(u.to_string()); + } + if let Some((rest, p)) = input.split_once(':') { + input = rest; + port = p.parse().ok() + } + hostname = Some(input.to_string()) + } + + let Some(hostname) = hostname else { + anyhow::bail!("missing hostname"); + }; + + let port_forwards = match port_forwards.len() { + 0 => None, + _ => Some(port_forwards), + }; + + Ok(Self { + host: hostname, + username, + port, + port_forwards, + args: Some(args), + password: None, + nickname: None, + upload_binary_over_ssh: false, + }) + } + + pub fn ssh_url(&self) -> String { + let mut result = String::from("ssh://"); + if let Some(username) = &self.username { + // Username might be: username1@username2@ip2 + let username = urlencoding::encode(username); + result.push_str(&username); + result.push('@'); + } + result.push_str(&self.host); + if let Some(port) = self.port { + result.push(':'); + result.push_str(&port.to_string()); + } + result + } + + pub fn additional_args(&self) -> Vec { + let mut args = self.args.iter().flatten().cloned().collect::>(); + + if let Some(forwards) = &self.port_forwards { + args.extend(forwards.iter().map(|pf| { + let local_host = match &pf.local_host { + Some(host) => host, + None => "localhost", + }; + let remote_host = match &pf.remote_host { + Some(host) => host, + None => "localhost", + }; + + format!( + "-L{}:{}:{}:{}", + local_host, pf.local_port, remote_host, pf.remote_port + ) + })); + } + + args + } + + fn scp_url(&self) -> String { + if let Some(username) = &self.username { + format!("{}@{}", username, self.host) + } else { + self.host.clone() + } + } + + pub fn connection_string(&self) -> String { + let host = if let Some(username) = &self.username { + format!("{}@{}", username, self.host) + } else { + self.host.clone() + }; + if let Some(port) = &self.port { + format!("{}:{}", host, port) + } else { + host + } + } +} diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6216ff7728..04028ebcac 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -21,7 +21,7 @@ use project::{ }; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, SSH_PEER_ID, SSH_PROJECT_ID}, + proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID}, }; use settings::initial_server_settings_content; @@ -83,7 +83,7 @@ impl HeadlessProject { let worktree_store = cx.new(|cx| { let mut store = WorktreeStore::local(true, fs.clone()); - store.shared(SSH_PROJECT_ID, session.clone(), cx); + store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); store }); @@ -101,7 +101,7 @@ impl HeadlessProject { let buffer_store = cx.new(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); - buffer_store.shared(SSH_PROJECT_ID, session.clone(), cx); + buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); buffer_store }); @@ -119,7 +119,7 @@ impl HeadlessProject { breakpoint_store.clone(), cx, ); - dap_store.shared(SSH_PROJECT_ID, session.clone(), cx); + dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); dap_store }); @@ -131,7 +131,7 @@ impl HeadlessProject { fs.clone(), cx, ); - store.shared(SSH_PROJECT_ID, session.clone(), cx); + store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); store }); @@ -154,7 +154,7 @@ impl HeadlessProject { environment.clone(), cx, ); - task_store.shared(SSH_PROJECT_ID, session.clone(), cx); + task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); task_store }); let settings_observer = cx.new(|cx| { @@ -164,7 +164,7 @@ impl HeadlessProject { task_store.clone(), cx, ); - observer.shared(SSH_PROJECT_ID, session.clone(), cx); + observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); observer }); @@ -185,7 +185,7 @@ impl HeadlessProject { fs.clone(), cx, ); - lsp_store.shared(SSH_PROJECT_ID, session.clone(), cx); + lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); lsp_store }); @@ -213,15 +213,15 @@ impl HeadlessProject { ); // local_machine -> ssh handlers - session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); - session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &task_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &dap_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); - session.subscribe_to_entity(SSH_PROJECT_ID, &git_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity()); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store); session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); @@ -288,7 +288,7 @@ impl HeadlessProject { } = event { cx.background_spawn(self.session.request(proto::UpdateBuffer { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, buffer_id: buffer.read(cx).remote_id().to_proto(), operations: vec![serialize_operation(operation)], })) @@ -310,7 +310,7 @@ impl HeadlessProject { } => { self.session .send(proto::UpdateLanguageServer { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, server_name: name.as_ref().map(|name| name.to_string()), language_server_id: language_server_id.to_proto(), variant: Some(message.clone()), @@ -320,7 +320,7 @@ impl HeadlessProject { LspStoreEvent::Notification(message) => { self.session .send(proto::Toast { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, notification_id: "lsp".to_string(), message: message.clone(), }) @@ -329,7 +329,7 @@ impl HeadlessProject { LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => { self.session .send(proto::LanguageServerLog { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, language_server_id: language_server_id.to_proto(), message: message.clone(), log_type: Some(log_type.to_proto()), @@ -338,7 +338,7 @@ impl HeadlessProject { } LspStoreEvent::LanguageServerPrompt(prompt) => { let request = self.session.request(proto::LanguageServerPromptRequest { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, actions: prompt .actions .iter() @@ -474,7 +474,7 @@ impl HeadlessProject { let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; buffer_store.update(&mut cx, |buffer_store, cx| { buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) .detach_and_log_err(cx); })?; @@ -500,7 +500,7 @@ impl HeadlessProject { let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; buffer_store.update(&mut cx, |buffer_store, cx| { buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) .detach_and_log_err(cx); })?; @@ -550,7 +550,7 @@ impl HeadlessProject { buffer_store.update(cx, |buffer_store, cx| { buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) .detach_and_log_err(cx); }); @@ -586,7 +586,7 @@ impl HeadlessProject { response.buffer_ids.push(buffer_id.to_proto()); buffer_store .update(&mut cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) })? .await?; } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 69fae7f399..e106a5ef18 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -22,7 +22,7 @@ use project::{ Project, ProjectPath, search::{SearchQuery, SearchResult}, }; -use remote::SshRemoteClient; +use remote::RemoteClient; use serde_json::json; use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; use smol::stream::StreamExt; @@ -1119,7 +1119,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) buffer.edit([(ix..ix + 1, "100")], None, cx); }); - let client = cx.read(|cx| project.read(cx).ssh_client().unwrap()); + let client = cx.read(|cx| project.read(cx).remote_client().unwrap()); client .update(cx, |client, cx| client.simulate_disconnect(cx)) .detach(); @@ -1782,7 +1782,7 @@ pub async fn init_test( }); init_logger(); - let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx); + let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx); let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); @@ -1804,7 +1804,7 @@ pub async fn init_test( ) }); - let ssh = SshRemoteClient::fake_client(opts, cx).await; + let ssh = RemoteClient::fake_client(opts, cx).await; let project = build_project(ssh, cx); project .update(cx, { @@ -1819,7 +1819,7 @@ fn init_logger() { zlog::init_test(); } -fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entity { +fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entity { cx.update(|cx| { if !cx.has_global::() { let settings_store = SettingsStore::test(cx); @@ -1845,5 +1845,5 @@ fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entit language::init(cx); }); - cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx)) + cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx)) } diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index b8a7351552..5c71b3155c 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -19,7 +19,7 @@ use project::project_settings::ProjectSettings; use proto::CrashReport; use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel}; -use remote::SshRemoteClient; +use remote::RemoteClient; use remote::{ json_log::LogRecord, protocol::{read_message, write_message}, @@ -394,7 +394,7 @@ fn start_server( }) .detach(); - SshRemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server") + RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server") } fn init_paths() -> anyhow::Result<()> { @@ -762,34 +762,21 @@ where R: AsyncRead + Unpin, W: AsyncWrite + Unpin, { - use remote::protocol::read_message_raw; + use remote::protocol::{read_message_raw, write_size_prefixed_buffer}; let mut buffer = Vec::new(); loop { read_message_raw(&mut reader, &mut buffer) .await .with_context(|| format!("failed to read message from {}", socket_name))?; - write_size_prefixed_buffer(&mut writer, &mut buffer) .await .with_context(|| format!("failed to write message to {}", socket_name))?; - writer.flush().await?; - buffer.clear(); } } -async fn write_size_prefixed_buffer( - stream: &mut S, - buffer: &mut Vec, -) -> Result<()> { - let len = buffer.len() as u32; - stream.write_all(len.to_le_bytes().as_slice()).await?; - stream.write_all(buffer).await?; - Ok(()) -} - fn initialize_settings( session: AnyProtoClient, fs: Arc, diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index fe3301fb89..56715b604e 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1403,7 +1403,7 @@ impl InputHandler for TerminalInputHandler { window.invalidate_character_coordinates(); let project = this.project().read(cx); let telemetry = project.client().telemetry().clone(); - telemetry.log_edit_event("terminal", project.is_via_ssh()); + telemetry.log_edit_event("terminal", project.is_via_remote_server()); }) .ok(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6b17911487..33402b23a6 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -484,7 +484,9 @@ impl TerminalPanel { let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| { let project = workspace.project().read(cx); ( - project.ssh_client().and_then(|it| it.read(cx).ssh_info()), + project + .remote_client() + .and_then(|it| it.read(cx).ssh_info()), project.is_via_collab(), ) }) else { diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index c667edb509..78f22faa13 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -337,7 +337,7 @@ impl TitleBar { let room = room.read(cx); let project = self.project.read(cx); - let is_local = project.is_local() || project.is_via_ssh(); + let is_local = project.is_local() || project.is_via_remote_server(); let is_shared = is_local && project.is_shared(); let is_muted = room.is_muted(); let muted_by_user = room.muted_by_user(); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b84a2800b6..318ad5cd2c 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -299,8 +299,8 @@ impl TitleBar { } } - fn render_ssh_project_host(&self, cx: &mut Context) -> Option { - let options = self.project.read(cx).ssh_connection_options(cx)?; + fn render_remote_project_connection(&self, cx: &mut Context) -> Option { + let options = self.project.read(cx).remote_connection_options(cx)?; let host: SharedString = options.connection_string().into(); let nickname = options @@ -308,7 +308,7 @@ impl TitleBar { .map(|nick| nick.into()) .unwrap_or_else(|| host.clone()); - let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? { + let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? { remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")), remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")), remote::ConnectionState::HeartbeatMissed => ( @@ -324,7 +324,7 @@ impl TitleBar { } }; - let icon_color = match self.project.read(cx).ssh_connection_state(cx)? { + let icon_color = match self.project.read(cx).remote_connection_state(cx)? { remote::ConnectionState::Connecting => Color::Info, remote::ConnectionState::Connected => Color::Default, remote::ConnectionState::HeartbeatMissed => Color::Warning, @@ -379,8 +379,8 @@ impl TitleBar { } pub fn render_project_host(&self, cx: &mut Context) -> Option { - if self.project.read(cx).is_via_ssh() { - return self.render_ssh_project_host(cx); + if self.project.read(cx).is_via_remote_server() { + return self.render_remote_project_connection(cx); } if self.project.read(cx).is_disconnected(cx) { diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 32d066c7eb..71394c874a 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -20,7 +20,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { - match self.project.read(cx).ssh_connection_state(cx) { + match self.project.read(cx).remote_connection_state(cx) { None | Some(ConnectionState::Connected) => {} Some( ConnectionState::Connecting diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bf58786d67..0b5b3cc361 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -74,7 +74,7 @@ use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier}; +use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -2073,7 +2073,7 @@ impl Workspace { cx: &mut Context, ) -> oneshot::Receiver>> { if self.project.read(cx).is_via_collab() - || self.project.read(cx).is_via_ssh() + || self.project.read(cx).is_via_remote_server() || !WorkspaceSettings::get_global(cx).use_system_path_prompts { let prompt = self.on_prompt_for_new_path.take().unwrap(); @@ -5284,7 +5284,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).ssh_connection_options(cx) { + if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { WorkspaceLocation::Location( SerializedWorkspaceLocation::Ssh(SerializedSshConnection { host: connection.host, @@ -6938,7 +6938,7 @@ async fn join_channel_internal( return None; } - if (project.is_local() || project.is_via_ssh()) + if (project.is_local() || project.is_via_remote_server()) && project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() @@ -7284,7 +7284,7 @@ pub fn open_ssh_project_with_new_connection( window: WindowHandle, connection_options: SshConnectionOptions, cancel_rx: oneshot::Receiver<()>, - delegate: Arc, + delegate: Arc, app_state: Arc, paths: Vec, cx: &mut App, @@ -7295,7 +7295,7 @@ pub fn open_ssh_project_with_new_connection( let session = match cx .update(|cx| { - remote::SshRemoteClient::new( + remote::RemoteClient::ssh( ConnectionIdentifier::Workspace(workspace_id.0), connection_options, cancel_rx, @@ -7310,7 +7310,7 @@ pub fn open_ssh_project_with_new_connection( }; let project = cx.update(|cx| { - project::Project::ssh( + project::Project::remote( session, app_state.client.clone(), app_state.node_runtime.clone(), diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index ac06f1fd9f..9c12a5f146 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -220,10 +220,10 @@ pub fn init( let installation_id = installation_id.clone(); let system_id = system_id.clone(); - let Some(ssh_client) = project.ssh_client() else { + let Some(remote_client) = project.remote_client() else { return; }; - ssh_client.update(cx, |client, cx| { + remote_client.update(cx, |client, cx| { if !TelemetrySettings::get_global(cx).diagnostics { return; } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1b9657dcc6..4f6c939129 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -918,7 +918,7 @@ fn register_actions( capture_audio(workspace, window, cx); }); - if workspace.project().read(cx).is_via_ssh() { + if workspace.project().read(cx).is_via_remote_server() { workspace.register_action({ move |workspace, _: &OpenServerSettings, window, cx| { let open_server_settings = workspace @@ -1543,7 +1543,7 @@ pub fn open_new_ssh_project_from_project( cx: &mut Context, ) -> Task> { let app_state = workspace.app_state().clone(); - let Some(ssh_client) = workspace.project().read(cx).ssh_client() else { + let Some(ssh_client) = workspace.project().read(cx).remote_client() else { return Task::ready(Err(anyhow::anyhow!("Not an ssh project"))); }; let connection_options = ssh_client.read(cx).connection_options();