From ea363466aa8e5ca9553820c3bde2746c56dfc6ea Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:03:42 -0400 Subject: [PATCH] Fix attach modal showing local processes in SSH sessions (#37608) Closes #37520 This change makes the attach modal load processes from the remote server when connecting via SSH, rather than showing local processes from the client machine. This works by using the new GetProcessesRequest RPC message to allow downstream clients to get the correct processes to display. It also only works with downstream ssh clients because the message handler is only registered on headless projects. Release Notes: - debugger: Fix bug where SSH attach modal showed local processes instead of processes from the server --- crates/debugger_ui/src/attach_modal.rs | 92 +++++++++++++++----- crates/debugger_ui/src/new_process_modal.rs | 9 +- crates/proto/proto/debugger.proto | 14 +++ crates/proto/proto/zed.proto | 5 +- crates/proto/src/proto.rs | 4 + crates/remote_server/src/headless_project.rs | 30 +++++++ 6 files changed, 130 insertions(+), 24 deletions(-) diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 662a98c82075cd6e936988959c855eadb5138092..3e3bc3ec27c3d1dbf0bacd445b883a50370d5b6f 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,8 +1,10 @@ use dap::{DapRegistry, DebugRequest}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render}; +use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task}; use gpui::{Subscription, WeakEntity}; use picker::{Picker, PickerDelegate}; +use project::Project; +use rpc::proto; use task::ZedDebugConfig; use util::debug_panic; @@ -56,29 +58,28 @@ impl AttachModal { pub fn new( definition: ZedDebugConfig, workspace: WeakEntity, + project: Entity, modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { - let mut processes: Box<[_]> = System::new_all() - .processes() - .values() - .map(|process| { - let name = process.name().to_string_lossy().into_owned(); - Candidate { - name: name.into(), - pid: process.pid().as_u32(), - command: process - .cmd() - .iter() - .map(|s| s.to_string_lossy().to_string()) - .collect::>(), - } - }) - .collect(); - processes.sort_by_key(|k| k.name.clone()); - let processes = processes.into_iter().collect(); - Self::with_processes(workspace, definition, processes, modal, window, cx) + let processes_task = get_processes_for_project(&project, cx); + + let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx); + + cx.spawn_in(window, async move |this, cx| { + let processes = processes_task.await; + this.update_in(cx, |modal, window, cx| { + modal.picker.update(cx, |picker, cx| { + picker.delegate.candidates = processes; + picker.refresh(window, cx); + }); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + modal } pub(super) fn with_processes( @@ -332,6 +333,57 @@ impl PickerDelegate for AttachModalDelegate { } } +fn get_processes_for_project(project: &Entity, cx: &mut App) -> Task> { + let project = project.read(cx); + + if let Some(remote_client) = project.remote_client() { + let proto_client = remote_client.read(cx).proto_client(); + cx.spawn(async move |_cx| { + let response = proto_client + .request(proto::GetProcesses { + project_id: proto::REMOTE_SERVER_PROJECT_ID, + }) + .await + .unwrap_or_else(|_| proto::GetProcessesResponse { + processes: Vec::new(), + }); + + let mut processes: Vec = response + .processes + .into_iter() + .map(|p| Candidate { + pid: p.pid, + name: p.name.into(), + command: p.command, + }) + .collect(); + + processes.sort_by_key(|k| k.name.clone()); + Arc::from(processes.into_boxed_slice()) + }) + } else { + let mut processes: Box<[_]> = System::new_all() + .processes() + .values() + .map(|process| { + let name = process.name().to_string_lossy().into_owned(); + Candidate { + name: name.into(), + pid: process.pid().as_u32(), + command: process + .cmd() + .iter() + .map(|s| s.to_string_lossy().to_string()) + .collect::>(), + } + }) + .collect(); + processes.sort_by_key(|k| k.name.clone()); + let processes = processes.into_iter().collect(); + Task::ready(processes) + } +} + #[cfg(any(test, feature = "test-support"))] pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context) -> Vec { modal.picker.read_with(cx, |picker, _| { diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 68770bc8b15fbf95824de167dbc8d7fada2b5075..ee6289187ba990d5bbaa040631a1c32619857e53 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -20,7 +20,7 @@ use gpui::{ }; use itertools::Itertools as _; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; -use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore}; +use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore}; use settings::Settings; use task::{DebugScenario, RevealTarget, ZedDebugConfig}; use theme::ThemeSettings; @@ -88,8 +88,10 @@ impl NewProcessModal { })?; workspace.update_in(cx, |workspace, window, cx| { let workspace_handle = workspace.weak_handle(); + let project = workspace.project().clone(); workspace.toggle_modal(window, cx, |window, cx| { - let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx); + let attach_mode = + AttachMode::new(None, workspace_handle.clone(), project, window, cx); let debug_picker = cx.new(|cx| { let delegate = @@ -940,6 +942,7 @@ impl AttachMode { pub(super) fn new( debugger: Option, workspace: WeakEntity, + project: Entity, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -950,7 +953,7 @@ impl AttachMode { stop_on_entry: Some(false), }; let attach_picker = cx.new(|cx| { - let modal = AttachModal::new(definition.clone(), workspace, false, window, cx); + let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx); window.focus(&modal.focus_handle(cx)); modal diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto index c6f9c9f1342336c36ab8dfd0ec70a24ff6564476..e3cb5ebbce0ceb87a7197f19a133bbb92a572085 100644 --- a/crates/proto/proto/debugger.proto +++ b/crates/proto/proto/debugger.proto @@ -546,3 +546,17 @@ message LogToDebugConsole { uint64 session_id = 2; string message = 3; } + +message GetProcesses { + uint64 project_id = 1; +} + +message GetProcessesResponse { + repeated ProcessInfo processes = 1; +} + +message ProcessInfo { + uint32 pid = 1; + string name = 2; + repeated string command = 3; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 4133b4b5eea6f14e2c9359f7318f192a8566d809..3763671a7a1f29949194d61c70866f96ca6ad972 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -399,7 +399,10 @@ message Envelope { LspQueryResponse lsp_query_response = 366; ToggleLspLogs toggle_lsp_logs = 367; - UpdateUserSettings update_user_settings = 368; // current max + UpdateUserSettings update_user_settings = 368; + + GetProcesses get_processes = 369; + GetProcessesResponse get_processes_response = 370; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 8f4e836b20ae5bae43617e10391f75c3a069a82f..3c98ae62e7a4b1489c071a0ac673d23b394c28d5 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -102,6 +102,8 @@ messages!( (GetPathMetadata, Background), (GetPathMetadataResponse, Background), (GetPermalinkToLine, Foreground), + (GetProcesses, Background), + (GetProcessesResponse, Background), (GetPermalinkToLineResponse, Foreground), (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), @@ -485,6 +487,7 @@ request_messages!( (GetDefaultBranch, GetDefaultBranchResponse), (GitClone, GitCloneResponse), (ToggleLspLogs, Ack), + (GetProcesses, GetProcessesResponse), ); lsp_messages!( @@ -610,6 +613,7 @@ entity_messages!( ActivateToolchain, ActiveToolchain, GetPathMetadata, + GetProcesses, CancelLanguageServerWork, RegisterBufferWithLanguageServers, GitShow, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index f55826631b46b4f9eaaa17d8a9f4b0603a07fcc3..7fb5ac8498c67863783b1944c912f0ad9767fed5 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -32,6 +32,7 @@ use std::{ path::{Path, PathBuf}, sync::{Arc, atomic::AtomicUsize}, }; +use sysinfo::System; use util::ResultExt; use worktree::Worktree; @@ -230,6 +231,7 @@ impl HeadlessProject { session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); session.add_request_handler(cx.weak_entity(), Self::handle_ping); + session.add_request_handler(cx.weak_entity(), Self::handle_get_processes); session.add_entity_request_handler(Self::handle_add_worktree); session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); @@ -719,6 +721,34 @@ impl HeadlessProject { log::debug!("Received ping from client"); Ok(proto::Ack {}) } + + async fn handle_get_processes( + _this: Entity, + _envelope: TypedEnvelope, + _cx: AsyncApp, + ) -> Result { + let mut processes = Vec::new(); + let system = System::new_all(); + + for (_pid, process) in system.processes() { + let name = process.name().to_string_lossy().into_owned(); + let command = process + .cmd() + .iter() + .map(|s| s.to_string_lossy().to_string()) + .collect::>(); + + processes.push(proto::ProcessInfo { + pid: process.pid().as_u32(), + name, + command, + }); + } + + processes.sort_by_key(|p| p.name.clone()); + + Ok(proto::GetProcessesResponse { processes }) + } } fn prompt_to_proto(