1use anyhow::{Context as _, Result, anyhow};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
7use fs::Fs;
8use futures::io::BufReader;
9use gpui::{AsyncApp, SharedString};
10pub use http_client::{HttpClient, github::latest_github_release};
11use language::{LanguageName, LanguageToolchainStore};
12use node_runtime::NodeRuntime;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use settings::WorktreeId;
16use smol::fs::File;
17use std::{
18 borrow::Borrow,
19 ffi::OsStr,
20 fmt::Debug,
21 net::Ipv4Addr,
22 ops::Deref,
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
27use util::{archive::extract_zip, rel_path::RelPath};
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum DapStatus {
31 None,
32 CheckingForUpdate,
33 Downloading,
34 Failed { error: String },
35}
36
37#[async_trait]
38pub trait DapDelegate: Send + Sync + 'static {
39 fn worktree_id(&self) -> WorktreeId;
40 fn worktree_root_path(&self) -> &Path;
41 fn http_client(&self) -> Arc<dyn HttpClient>;
42 fn node_runtime(&self) -> NodeRuntime;
43 fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
44 fn fs(&self) -> Arc<dyn Fs>;
45 fn output_to_console(&self, msg: String);
46 async fn which(&self, command: &OsStr) -> Option<PathBuf>;
47 async fn read_text_file(&self, path: &RelPath) -> Result<String>;
48 async fn shell_env(&self) -> collections::HashMap<String, String>;
49 fn is_headless(&self) -> bool;
50}
51
52#[derive(
53 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
54)]
55#[serde(transparent)]
56pub struct DebugAdapterName(pub SharedString);
57
58impl Deref for DebugAdapterName {
59 type Target = str;
60
61 fn deref(&self) -> &Self::Target {
62 &self.0
63 }
64}
65
66impl AsRef<str> for DebugAdapterName {
67 fn as_ref(&self) -> &str {
68 &self.0
69 }
70}
71
72impl Borrow<str> for DebugAdapterName {
73 fn borrow(&self) -> &str {
74 &self.0
75 }
76}
77
78impl Borrow<SharedString> for DebugAdapterName {
79 fn borrow(&self) -> &SharedString {
80 &self.0
81 }
82}
83
84impl std::fmt::Display for DebugAdapterName {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 std::fmt::Display::fmt(&self.0, f)
87 }
88}
89
90impl From<DebugAdapterName> for SharedString {
91 fn from(name: DebugAdapterName) -> Self {
92 name.0
93 }
94}
95impl From<SharedString> for DebugAdapterName {
96 fn from(name: SharedString) -> Self {
97 DebugAdapterName(name)
98 }
99}
100
101impl<'a> From<&'a str> for DebugAdapterName {
102 fn from(str: &'a str) -> DebugAdapterName {
103 DebugAdapterName(str.to_string().into())
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize)]
108pub struct TcpArguments {
109 pub host: Ipv4Addr,
110 pub port: u16,
111 pub timeout: Option<u64>,
112}
113
114impl TcpArguments {
115 pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
116 let host = TcpArgumentsTemplate::from_proto(proto)?;
117 Ok(TcpArguments {
118 host: host.host.context("missing host")?,
119 port: host.port.context("missing port")?,
120 timeout: host.timeout,
121 })
122 }
123
124 pub fn to_proto(&self) -> proto::TcpHost {
125 TcpArgumentsTemplate {
126 host: Some(self.host),
127 port: Some(self.port),
128 timeout: self.timeout,
129 }
130 .to_proto()
131 }
132}
133
134/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
135///
136/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
137/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
138/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
139#[derive(Clone, Debug, PartialEq)]
140#[cfg_attr(
141 any(feature = "test-support", test),
142 derive(serde::Deserialize, serde::Serialize)
143)]
144pub struct DebugTaskDefinition {
145 /// The name of this debug task
146 pub label: SharedString,
147 /// The debug adapter to use
148 pub adapter: DebugAdapterName,
149 /// The configuration to send to the debug adapter
150 pub config: serde_json::Value,
151 /// Optional TCP connection information
152 ///
153 /// If provided, this will be used to connect to the debug adapter instead of
154 /// spawning a new debug adapter process. This is useful for connecting to a debug adapter
155 /// that is already running or is started by another process.
156 pub tcp_connection: Option<TcpArgumentsTemplate>,
157}
158
159impl DebugTaskDefinition {
160 pub fn to_scenario(&self) -> DebugScenario {
161 DebugScenario {
162 label: self.label.clone(),
163 adapter: self.adapter.clone().into(),
164 build: None,
165 tcp_connection: self.tcp_connection.clone(),
166 config: self.config.clone(),
167 }
168 }
169
170 pub fn to_proto(&self) -> proto::DebugTaskDefinition {
171 proto::DebugTaskDefinition {
172 label: self.label.clone().into(),
173 config: self.config.to_string(),
174 tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
175 adapter: self.adapter.clone().0.into(),
176 }
177 }
178
179 pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
180 Ok(Self {
181 label: proto.label.into(),
182 config: serde_json::from_str(&proto.config)?,
183 tcp_connection: proto
184 .tcp_connection
185 .map(TcpArgumentsTemplate::from_proto)
186 .transpose()?,
187 adapter: DebugAdapterName(proto.adapter.into()),
188 })
189 }
190}
191
192/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
193#[derive(Debug, Clone, PartialEq, Serialize)]
194pub struct DebugAdapterBinary {
195 pub command: Option<String>,
196 pub arguments: Vec<String>,
197 pub envs: HashMap<String, String>,
198 pub cwd: Option<PathBuf>,
199 pub connection: Option<TcpArguments>,
200 pub request_args: StartDebuggingRequestArguments,
201}
202
203impl DebugAdapterBinary {
204 pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
205 let request = match binary.launch_type() {
206 proto::debug_adapter_binary::LaunchType::Launch => {
207 StartDebuggingRequestArgumentsRequest::Launch
208 }
209 proto::debug_adapter_binary::LaunchType::Attach => {
210 StartDebuggingRequestArgumentsRequest::Attach
211 }
212 };
213
214 Ok(DebugAdapterBinary {
215 command: binary.command,
216 arguments: binary.arguments,
217 envs: binary.envs.into_iter().collect(),
218 connection: binary
219 .connection
220 .map(TcpArguments::from_proto)
221 .transpose()?,
222 request_args: StartDebuggingRequestArguments {
223 configuration: serde_json::from_str(&binary.configuration)?,
224 request,
225 },
226 cwd: binary.cwd.map(|cwd| cwd.into()),
227 })
228 }
229
230 pub fn to_proto(&self) -> proto::DebugAdapterBinary {
231 proto::DebugAdapterBinary {
232 command: self.command.clone(),
233 arguments: self.arguments.clone(),
234 envs: self
235 .envs
236 .iter()
237 .map(|(k, v)| (k.clone(), v.clone()))
238 .collect(),
239 cwd: self
240 .cwd
241 .as_ref()
242 .map(|cwd| cwd.to_string_lossy().into_owned()),
243 connection: self.connection.as_ref().map(|c| c.to_proto()),
244 launch_type: match self.request_args.request {
245 StartDebuggingRequestArgumentsRequest::Launch => {
246 proto::debug_adapter_binary::LaunchType::Launch.into()
247 }
248 StartDebuggingRequestArgumentsRequest::Attach => {
249 proto::debug_adapter_binary::LaunchType::Attach.into()
250 }
251 },
252 configuration: self.request_args.configuration.to_string(),
253 }
254 }
255}
256
257#[derive(Debug, Clone)]
258pub struct AdapterVersion {
259 pub tag_name: String,
260 pub url: String,
261}
262
263pub enum DownloadedFileType {
264 Vsix,
265 GzipTar,
266 Zip,
267}
268
269pub struct GithubRepo {
270 pub repo_name: String,
271 pub repo_owner: String,
272}
273
274pub async fn download_adapter_from_github(
275 adapter_name: DebugAdapterName,
276 github_version: AdapterVersion,
277 file_type: DownloadedFileType,
278 delegate: &dyn DapDelegate,
279) -> Result<PathBuf> {
280 let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
281 let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
282 let fs = delegate.fs();
283
284 if version_path.exists() {
285 return Ok(version_path);
286 }
287
288 if !adapter_path.exists() {
289 fs.create_dir(adapter_path.as_path())
290 .await
291 .context("Failed creating adapter path")?;
292 }
293
294 log::debug!(
295 "Downloading adapter {} from {}",
296 adapter_name,
297 &github_version.url,
298 );
299 delegate.output_to_console(format!("Downloading from {}...", github_version.url));
300
301 let mut response = delegate
302 .http_client()
303 .get(&github_version.url, Default::default(), true)
304 .await
305 .context("Error downloading release")?;
306 anyhow::ensure!(
307 response.status().is_success(),
308 "download failed with status {}",
309 response.status().to_string()
310 );
311
312 delegate.output_to_console("Download complete".to_owned());
313 match file_type {
314 DownloadedFileType::GzipTar => {
315 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
316 let archive = Archive::new(decompressed_bytes);
317 archive.unpack(&version_path).await?;
318 }
319 DownloadedFileType::Zip | DownloadedFileType::Vsix => {
320 let zip_path = version_path.with_extension("zip");
321 let mut file = File::create(&zip_path).await?;
322 futures::io::copy(response.body_mut(), &mut file).await?;
323 let file = File::open(&zip_path).await?;
324 extract_zip(&version_path, file)
325 .await
326 // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
327 .ok();
328
329 util::fs::remove_matching(&adapter_path, |entry| {
330 entry
331 .file_name()
332 .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
333 })
334 .await;
335 }
336 }
337
338 // remove older versions
339 util::fs::remove_matching(&adapter_path, |entry| {
340 entry.to_string_lossy() != version_path.to_string_lossy()
341 })
342 .await;
343
344 Ok(version_path)
345}
346
347#[async_trait(?Send)]
348pub trait DebugAdapter: 'static + Send + Sync {
349 fn name(&self) -> DebugAdapterName;
350
351 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
352
353 async fn get_binary(
354 &self,
355 delegate: &Arc<dyn DapDelegate>,
356 config: &DebugTaskDefinition,
357 user_installed_path: Option<PathBuf>,
358 user_args: Option<Vec<String>>,
359 user_env: Option<HashMap<String, String>>,
360 cx: &mut AsyncApp,
361 ) -> Result<DebugAdapterBinary>;
362
363 /// Returns the language name of an adapter if it only supports one language
364 fn adapter_language_name(&self) -> Option<LanguageName> {
365 None
366 }
367
368 /// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
369 /// This method should only return error when the kind cannot be determined for a given configuration;
370 /// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
371 async fn request_kind(
372 &self,
373 config: &serde_json::Value,
374 ) -> Result<StartDebuggingRequestArgumentsRequest> {
375 match config.get("request") {
376 Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
377 Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
378 _ => Err(anyhow!(
379 "missing or invalid `request` field in config. Expected 'launch' or 'attach'"
380 )),
381 }
382 }
383
384 fn dap_schema(&self) -> serde_json::Value;
385
386 fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
387 None
388 }
389
390 fn compact_child_session(&self) -> bool {
391 false
392 }
393
394 fn prefer_thread_name(&self) -> bool {
395 false
396 }
397}
398
399#[cfg(any(test, feature = "test-support"))]
400pub struct FakeAdapter {}
401
402#[cfg(any(test, feature = "test-support"))]
403impl FakeAdapter {
404 pub const ADAPTER_NAME: &'static str = "fake-adapter";
405
406 pub fn new() -> Self {
407 Self {}
408 }
409}
410
411#[cfg(any(test, feature = "test-support"))]
412#[async_trait(?Send)]
413impl DebugAdapter for FakeAdapter {
414 fn name(&self) -> DebugAdapterName {
415 DebugAdapterName(Self::ADAPTER_NAME.into())
416 }
417
418 fn dap_schema(&self) -> serde_json::Value {
419 serde_json::Value::Null
420 }
421
422 async fn request_kind(
423 &self,
424 config: &serde_json::Value,
425 ) -> Result<StartDebuggingRequestArgumentsRequest> {
426 let request = config.as_object().unwrap()["request"].as_str().unwrap();
427
428 let request = match request {
429 "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
430 "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
431 _ => unreachable!("Wrong fake adapter input for request field"),
432 };
433
434 Ok(request)
435 }
436
437 fn adapter_language_name(&self) -> Option<LanguageName> {
438 None
439 }
440
441 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
442 let config = serde_json::to_value(zed_scenario.request).unwrap();
443
444 Ok(DebugScenario {
445 adapter: zed_scenario.adapter,
446 label: zed_scenario.label,
447 build: None,
448 config,
449 tcp_connection: None,
450 })
451 }
452
453 async fn get_binary(
454 &self,
455 _: &Arc<dyn DapDelegate>,
456 task_definition: &DebugTaskDefinition,
457 _: Option<PathBuf>,
458 _: Option<Vec<String>>,
459 _: Option<HashMap<String, String>>,
460 _: &mut AsyncApp,
461 ) -> Result<DebugAdapterBinary> {
462 let connection = task_definition
463 .tcp_connection
464 .as_ref()
465 .map(|connection| TcpArguments {
466 host: connection.host(),
467 port: connection.port.unwrap_or(17),
468 timeout: connection.timeout,
469 });
470 Ok(DebugAdapterBinary {
471 command: Some("command".into()),
472 arguments: vec![],
473 connection,
474 envs: HashMap::default(),
475 cwd: None,
476 request_args: StartDebuggingRequestArguments {
477 request: self.request_kind(&task_definition.config).await?,
478 configuration: task_definition.config.clone(),
479 },
480 })
481 }
482}