ZIm/crates/dap/src/client.rs
Conrad Irwin 109651e6e9
debugger: Fix connections over SSH (#32834)
Before this change, we would see "connection reset" when sending the
initialize
request over SSH in the case that the debug adapter was slow to boot.

(Although we'd have successfully created a connection to the local SSH
port,
trying to read/write from it would not work until the remote end of the
connection had been established)

Fixes  #32575

Release Notes:

- debugger: Fix connecting to a Python debugger over SSH
2025-06-17 06:48:17 +00:00

439 lines
14 KiB
Rust

use crate::{
adapters::DebugAdapterBinary,
transport::{IoKind, LogKind, TransportDelegate},
};
use anyhow::Result;
use dap_types::{
messages::{Message, Response},
requests::Request,
};
use futures::channel::oneshot;
use gpui::AsyncApp;
use std::{
hash::Hash,
sync::atomic::{AtomicU64, Ordering},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SessionId(pub u32);
impl SessionId {
pub fn from_proto(client_id: u64) -> Self {
Self(client_id as u32)
}
pub fn to_proto(&self) -> u64 {
self.0 as u64
}
}
/// Represents a connection to the debug adapter process, either via stdout/stdin or a socket.
pub struct DebugAdapterClient {
id: SessionId,
sequence_count: AtomicU64,
binary: DebugAdapterBinary,
transport_delegate: TransportDelegate,
}
pub type DapMessageHandler = Box<dyn FnMut(Message) + 'static + Send + Sync>;
impl DebugAdapterClient {
pub async fn start(
id: SessionId,
binary: DebugAdapterBinary,
message_handler: DapMessageHandler,
cx: &mut AsyncApp,
) -> Result<Self> {
let transport_delegate = TransportDelegate::start(&binary, cx).await?;
let this = Self {
id,
binary,
transport_delegate,
sequence_count: AtomicU64::new(1),
};
this.connect(message_handler, cx).await?;
Ok(this)
}
pub fn should_reconnect_for_ssh(&self) -> bool {
self.transport_delegate.tcp_arguments().is_some()
&& self.binary.command.as_deref() == Some("ssh")
}
pub async fn connect(
&self,
message_handler: DapMessageHandler,
cx: &mut AsyncApp,
) -> Result<()> {
self.transport_delegate.connect(message_handler, cx).await
}
pub async fn create_child_connection(
&self,
session_id: SessionId,
binary: DebugAdapterBinary,
message_handler: DapMessageHandler,
cx: &mut AsyncApp,
) -> Result<Self> {
let binary = if let Some(connection) = self.transport_delegate.tcp_arguments() {
DebugAdapterBinary {
command: None,
arguments: Default::default(),
envs: Default::default(),
cwd: Default::default(),
connection: Some(connection),
request_args: binary.request_args,
}
} else {
self.binary.clone()
};
Self::start(session_id, binary, message_handler, cx).await
}
/// Send a request to an adapter and get a response back
/// Note: This function will block until a response is sent back from the adapter
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
let serialized_arguments = serde_json::to_value(arguments)?;
let (callback_tx, callback_rx) = oneshot::channel::<Result<Response>>();
let sequence_id = self.next_sequence_id();
let request = crate::messages::Request {
seq: sequence_id,
command: R::COMMAND.to_string(),
arguments: Some(serialized_arguments),
};
self.transport_delegate
.add_pending_request(sequence_id, callback_tx);
log::debug!(
"Client {} send `{}` request with sequence_id: {}",
self.id.0,
R::COMMAND,
sequence_id
);
self.send_message(Message::Request(request)).await?;
let command = R::COMMAND.to_string();
let response = callback_rx.await??;
log::debug!(
"Client {} received response for: `{}` sequence_id: {}",
self.id.0,
command,
sequence_id
);
match response.success {
true => {
if let Some(json) = response.body {
Ok(serde_json::from_value(json)?)
// Note: dap types configure themselves to return `None` when an empty object is received,
// which then fails here...
} else if let Ok(result) =
serde_json::from_value(serde_json::Value::Object(Default::default()))
{
Ok(result)
} else {
Ok(serde_json::from_value(Default::default())?)
}
}
false => anyhow::bail!("Request failed: {}", response.message.unwrap_or_default()),
}
}
pub async fn send_message(&self, message: Message) -> Result<()> {
self.transport_delegate.send_message(message).await
}
pub fn id(&self) -> SessionId {
self.id
}
pub fn binary(&self) -> &DebugAdapterBinary {
&self.binary
}
/// Get the next sequence id to be used in a request
pub fn next_sequence_id(&self) -> u64 {
self.sequence_count.fetch_add(1, Ordering::Relaxed)
}
pub async fn shutdown(&self) -> Result<()> {
self.transport_delegate.shutdown().await
}
pub fn has_adapter_logs(&self) -> bool {
self.transport_delegate.has_adapter_logs()
}
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
where
F: 'static + Send + FnMut(IoKind, Option<&str>, &str),
{
self.transport_delegate.add_log_handler(f, kind);
}
#[cfg(any(test, feature = "test-support"))]
pub fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
where
F: 'static
+ Send
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
{
self.transport_delegate
.transport
.lock()
.as_fake()
.on_request::<R, F>(handler);
}
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_reverse_request<R: dap_types::requests::Request>(&self, args: R::Arguments) {
self.send_message(Message::Request(dap_types::messages::Request {
seq: self.sequence_count.load(Ordering::Relaxed),
command: R::COMMAND.into(),
arguments: serde_json::to_value(args).ok(),
}))
.await
.unwrap();
}
#[cfg(any(test, feature = "test-support"))]
pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F)
where
F: 'static + Send + Fn(Response),
{
self.transport_delegate
.transport
.lock()
.as_fake()
.on_response::<R, F>(handler);
}
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_event(&self, event: dap_types::messages::Events) {
self.send_message(Message::Event(Box::new(event)))
.await
.unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{client::DebugAdapterClient, debugger_settings::DebuggerSettings};
use dap_types::{
Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat,
RunInTerminalRequestArguments, StartDebuggingRequestArguments,
messages::Events,
requests::{Initialize, Request, RunInTerminal},
};
use gpui::TestAppContext;
use serde_json::json;
use settings::{Settings, SettingsStore};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
pub fn init_test(cx: &mut gpui::TestAppContext) {
zlog::init_test();
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
DebuggerSettings::register(cx);
});
}
#[gpui::test]
pub async fn test_initialize_client(cx: &mut TestAppContext) {
init_test(cx);
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
cwd: None,
request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Null,
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new(|_| panic!("Did not expect to hit this code path")),
&mut cx.to_async(),
)
.await
.unwrap();
client.on_request::<Initialize, _>(move |_, _| {
Ok(dap_types::Capabilities {
supports_configuration_done_request: Some(true),
..Default::default()
})
});
cx.run_until_parked();
let response = client
.request::<Initialize>(InitializeRequestArguments {
client_id: Some("zed".to_owned()),
client_name: Some("Zed".to_owned()),
adapter_id: "fake-adapter".to_owned(),
locale: Some("en-US".to_owned()),
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
supports_variable_type: Some(true),
supports_variable_paging: Some(false),
supports_run_in_terminal_request: Some(true),
supports_memory_references: Some(true),
supports_progress_reporting: Some(false),
supports_invalidated_event: Some(false),
lines_start_at1: Some(true),
columns_start_at1: Some(true),
supports_memory_event: Some(false),
supports_args_can_be_interpreted_by_shell: Some(false),
supports_start_debugging_request: Some(true),
supports_ansistyling: Some(false),
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
dap_types::Capabilities {
supports_configuration_done_request: Some(true),
..Default::default()
},
response
);
client.shutdown().await.unwrap();
}
#[gpui::test]
pub async fn test_calls_event_handler(cx: &mut TestAppContext) {
init_test(cx);
let called_event_handler = Arc::new(AtomicBool::new(false));
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
cwd: None,
request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Null,
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new({
let called_event_handler = called_event_handler.clone();
move |event| {
called_event_handler.store(true, Ordering::SeqCst);
assert_eq!(
Message::Event(Box::new(Events::Initialized(
Some(Capabilities::default())
))),
event
);
}
}),
&mut cx.to_async(),
)
.await
.unwrap();
cx.run_until_parked();
client
.fake_event(Events::Initialized(Some(Capabilities::default())))
.await;
cx.run_until_parked();
assert!(
called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
"Event handler was not called"
);
client.shutdown().await.unwrap();
}
#[gpui::test]
pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) {
init_test(cx);
let called_event_handler = Arc::new(AtomicBool::new(false));
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
cwd: None,
request_args: dap_types::StartDebuggingRequestArguments {
configuration: serde_json::Value::Null,
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new({
let called_event_handler = called_event_handler.clone();
move |event| {
called_event_handler.store(true, Ordering::SeqCst);
assert_eq!(
Message::Request(dap_types::messages::Request {
seq: 1,
command: RunInTerminal::COMMAND.into(),
arguments: Some(json!({
"cwd": "/project/path/src",
"args": ["node", "test.js"],
}))
}),
event
);
}
}),
&mut cx.to_async(),
)
.await
.unwrap();
cx.run_until_parked();
client
.fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
kind: None,
title: None,
cwd: "/project/path/src".into(),
args: vec!["node".into(), "test.js".into()],
env: None,
args_can_be_interpreted_by_shell: None,
})
.await;
cx.run_until_parked();
assert!(
called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
"Event handler was not called"
);
client.shutdown().await.unwrap();
}
}