Fix attach modal showing local processes in SSH sessions (#37608)

Anthony Eid created

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

Change summary

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(-)

Detailed changes

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<Workspace>,
+        project: Entity<Project>,
         modal: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> 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::<Vec<_>>(),
-                }
-            })
-            .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<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
+    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<Candidate> = 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::<Vec<_>>(),
+                }
+            })
+            .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<AttachModal>) -> Vec<String> {
     modal.picker.read_with(cx, |picker, _| {

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<DebugAdapterName>,
         workspace: WeakEntity<Workspace>,
+        project: Entity<Project>,
         window: &mut Window,
         cx: &mut Context<NewProcessModal>,
     ) -> Entity<Self> {
@@ -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

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;
+}

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;

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,

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<Self>,
+        _envelope: TypedEnvelope<proto::GetProcesses>,
+        _cx: AsyncApp,
+    ) -> Result<proto::GetProcessesResponse> {
+        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::<Vec<_>>();
+
+            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(