1use ::fs::Fs;
2use anyhow::{Context as _, Result, anyhow};
3use async_compression::futures::bufread::GzipDecoder;
4use async_tar::Archive;
5use async_trait::async_trait;
6use collections::HashMap;
7use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
8use futures::io::BufReader;
9use gpui::{AsyncApp, SharedString};
10pub use http_client::{HttpClient, github::latest_github_release};
11use language::LanguageToolchainStore;
12use node_runtime::NodeRuntime;
13use serde::{Deserialize, Serialize};
14use settings::WorktreeId;
15use smol::{self, fs::File, lock::Mutex};
16use std::{
17 borrow::Borrow,
18 collections::HashSet,
19 ffi::OsStr,
20 fmt::Debug,
21 net::Ipv4Addr,
22 ops::Deref,
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
27use util::ResultExt;
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(?Send)]
38pub trait DapDelegate {
39 fn worktree_id(&self) -> WorktreeId;
40 fn http_client(&self) -> Arc<dyn HttpClient>;
41 fn node_runtime(&self) -> NodeRuntime;
42 fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
43 fn fs(&self) -> Arc<dyn Fs>;
44 fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
45 fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
46 fn which(&self, command: &OsStr) -> Option<PathBuf>;
47 async fn shell_env(&self) -> collections::HashMap<String, String>;
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
51pub struct DebugAdapterName(pub SharedString);
52
53impl Deref for DebugAdapterName {
54 type Target = str;
55
56 fn deref(&self) -> &Self::Target {
57 &self.0
58 }
59}
60
61impl AsRef<str> for DebugAdapterName {
62 fn as_ref(&self) -> &str {
63 &self.0
64 }
65}
66
67impl Borrow<str> for DebugAdapterName {
68 fn borrow(&self) -> &str {
69 &self.0
70 }
71}
72
73impl std::fmt::Display for DebugAdapterName {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 std::fmt::Display::fmt(&self.0, f)
76 }
77}
78
79impl From<DebugAdapterName> for SharedString {
80 fn from(name: DebugAdapterName) -> Self {
81 name.0
82 }
83}
84
85impl<'a> From<&'a str> for DebugAdapterName {
86 fn from(str: &'a str) -> DebugAdapterName {
87 DebugAdapterName(str.to_string().into())
88 }
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct TcpArguments {
93 pub host: Ipv4Addr,
94 pub port: u16,
95 pub timeout: Option<u64>,
96}
97
98impl TcpArguments {
99 pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
100 let host = TcpArgumentsTemplate::from_proto(proto)?;
101 Ok(TcpArguments {
102 host: host.host.ok_or_else(|| anyhow!("missing host"))?,
103 port: host.port.ok_or_else(|| anyhow!("missing port"))?,
104 timeout: host.timeout,
105 })
106 }
107
108 pub fn to_proto(&self) -> proto::TcpHost {
109 TcpArgumentsTemplate {
110 host: Some(self.host),
111 port: Some(self.port),
112 timeout: self.timeout,
113 }
114 .to_proto()
115 }
116}
117
118/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
119///
120/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
121/// 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.
122/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
123#[derive(Clone, Debug, PartialEq)]
124#[cfg_attr(
125 any(feature = "test-support", test),
126 derive(serde::Deserialize, serde::Serialize)
127)]
128pub struct DebugTaskDefinition {
129 pub label: SharedString,
130 pub adapter: DebugAdapterName,
131 pub request: DebugRequest,
132 /// Additional initialization arguments to be sent on DAP initialization
133 pub initialize_args: Option<serde_json::Value>,
134 /// Whether to tell the debug adapter to stop on entry
135 pub stop_on_entry: Option<bool>,
136 /// Optional TCP connection information
137 ///
138 /// If provided, this will be used to connect to the debug adapter instead of
139 /// spawning a new debug adapter process. This is useful for connecting to a debug adapter
140 /// that is already running or is started by another process.
141 pub tcp_connection: Option<TcpArgumentsTemplate>,
142}
143
144impl DebugTaskDefinition {
145 pub fn cwd(&self) -> Option<&Path> {
146 if let DebugRequest::Launch(config) = &self.request {
147 config.cwd.as_ref().map(Path::new)
148 } else {
149 None
150 }
151 }
152
153 pub fn to_scenario(&self) -> DebugScenario {
154 DebugScenario {
155 label: self.label.clone(),
156 adapter: self.adapter.clone().into(),
157 build: None,
158 request: Some(self.request.clone()),
159 stop_on_entry: self.stop_on_entry,
160 tcp_connection: self.tcp_connection.clone(),
161 initialize_args: self.initialize_args.clone(),
162 }
163 }
164
165 pub fn to_proto(&self) -> proto::DebugTaskDefinition {
166 proto::DebugTaskDefinition {
167 adapter: self.adapter.to_string(),
168 request: Some(match &self.request {
169 DebugRequest::Launch(config) => {
170 proto::debug_task_definition::Request::DebugLaunchRequest(
171 proto::DebugLaunchRequest {
172 program: config.program.clone(),
173 cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
174 args: config.args.clone(),
175 env: config
176 .env
177 .iter()
178 .map(|(k, v)| (k.clone(), v.clone()))
179 .collect(),
180 },
181 )
182 }
183 DebugRequest::Attach(attach_request) => {
184 proto::debug_task_definition::Request::DebugAttachRequest(
185 proto::DebugAttachRequest {
186 process_id: attach_request.process_id.unwrap_or_default(),
187 },
188 )
189 }
190 }),
191 label: self.label.to_string(),
192 initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
193 tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
194 stop_on_entry: self.stop_on_entry,
195 }
196 }
197
198 pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
199 let request = proto
200 .request
201 .ok_or_else(|| anyhow::anyhow!("request is required"))?;
202 Ok(Self {
203 label: proto.label.into(),
204 initialize_args: proto.initialize_args.map(|v| v.into()),
205 tcp_connection: proto
206 .tcp_connection
207 .map(TcpArgumentsTemplate::from_proto)
208 .transpose()?,
209 stop_on_entry: proto.stop_on_entry,
210 adapter: DebugAdapterName(proto.adapter.into()),
211 request: match request {
212 proto::debug_task_definition::Request::DebugAttachRequest(config) => {
213 DebugRequest::Attach(AttachRequest {
214 process_id: Some(config.process_id),
215 })
216 }
217
218 proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
219 DebugRequest::Launch(LaunchRequest {
220 program: config.program,
221 cwd: config.cwd.map(|cwd| cwd.into()),
222 args: config.args,
223 env: Default::default(),
224 })
225 }
226 },
227 })
228 }
229}
230
231/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
232#[derive(Debug, Clone, PartialEq)]
233pub struct DebugAdapterBinary {
234 pub command: String,
235 pub arguments: Vec<String>,
236 pub envs: HashMap<String, String>,
237 pub cwd: Option<PathBuf>,
238 pub connection: Option<TcpArguments>,
239 pub request_args: StartDebuggingRequestArguments,
240}
241
242impl DebugAdapterBinary {
243 pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
244 let request = match binary.launch_type() {
245 proto::debug_adapter_binary::LaunchType::Launch => {
246 StartDebuggingRequestArgumentsRequest::Launch
247 }
248 proto::debug_adapter_binary::LaunchType::Attach => {
249 StartDebuggingRequestArgumentsRequest::Attach
250 }
251 };
252
253 Ok(DebugAdapterBinary {
254 command: binary.command,
255 arguments: binary.arguments,
256 envs: binary.envs.into_iter().collect(),
257 connection: binary
258 .connection
259 .map(TcpArguments::from_proto)
260 .transpose()?,
261 request_args: StartDebuggingRequestArguments {
262 configuration: serde_json::from_str(&binary.configuration)?,
263 request,
264 },
265 cwd: binary.cwd.map(|cwd| cwd.into()),
266 })
267 }
268
269 pub fn to_proto(&self) -> proto::DebugAdapterBinary {
270 proto::DebugAdapterBinary {
271 command: self.command.clone(),
272 arguments: self.arguments.clone(),
273 envs: self
274 .envs
275 .iter()
276 .map(|(k, v)| (k.clone(), v.clone()))
277 .collect(),
278 cwd: self
279 .cwd
280 .as_ref()
281 .map(|cwd| cwd.to_string_lossy().to_string()),
282 connection: self.connection.as_ref().map(|c| c.to_proto()),
283 launch_type: match self.request_args.request {
284 StartDebuggingRequestArgumentsRequest::Launch => {
285 proto::debug_adapter_binary::LaunchType::Launch.into()
286 }
287 StartDebuggingRequestArgumentsRequest::Attach => {
288 proto::debug_adapter_binary::LaunchType::Attach.into()
289 }
290 },
291 configuration: self.request_args.configuration.to_string(),
292 }
293 }
294}
295
296#[derive(Debug)]
297pub struct AdapterVersion {
298 pub tag_name: String,
299 pub url: String,
300}
301
302pub enum DownloadedFileType {
303 Vsix,
304 GzipTar,
305 Zip,
306}
307
308pub struct GithubRepo {
309 pub repo_name: String,
310 pub repo_owner: String,
311}
312
313pub async fn download_adapter_from_github(
314 adapter_name: DebugAdapterName,
315 github_version: AdapterVersion,
316 file_type: DownloadedFileType,
317 delegate: &dyn DapDelegate,
318) -> Result<PathBuf> {
319 let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
320 let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
321 let fs = delegate.fs();
322
323 if version_path.exists() {
324 return Ok(version_path);
325 }
326
327 if !adapter_path.exists() {
328 fs.create_dir(&adapter_path.as_path())
329 .await
330 .context("Failed creating adapter path")?;
331 }
332
333 log::debug!(
334 "Downloading adapter {} from {}",
335 adapter_name,
336 &github_version.url,
337 );
338
339 let mut response = delegate
340 .http_client()
341 .get(&github_version.url, Default::default(), true)
342 .await
343 .context("Error downloading release")?;
344 if !response.status().is_success() {
345 Err(anyhow!(
346 "download failed with status {}",
347 response.status().to_string()
348 ))?;
349 }
350
351 match file_type {
352 DownloadedFileType::GzipTar => {
353 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
354 let archive = Archive::new(decompressed_bytes);
355 archive.unpack(&version_path).await?;
356 }
357 DownloadedFileType::Zip | DownloadedFileType::Vsix => {
358 let zip_path = version_path.with_extension("zip");
359
360 let mut file = File::create(&zip_path).await?;
361 futures::io::copy(response.body_mut(), &mut file).await?;
362
363 // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
364 util::command::new_smol_command("unzip")
365 .arg(&zip_path)
366 .arg("-d")
367 .arg(&version_path)
368 .output()
369 .await?;
370
371 util::fs::remove_matching(&adapter_path, |entry| {
372 entry
373 .file_name()
374 .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
375 })
376 .await;
377 }
378 }
379
380 // remove older versions
381 util::fs::remove_matching(&adapter_path, |entry| {
382 entry.to_string_lossy() != version_path.to_string_lossy()
383 })
384 .await;
385
386 Ok(version_path)
387}
388
389pub async fn fetch_latest_adapter_version_from_github(
390 github_repo: GithubRepo,
391 delegate: &dyn DapDelegate,
392) -> Result<AdapterVersion> {
393 let release = latest_github_release(
394 &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
395 false,
396 false,
397 delegate.http_client(),
398 )
399 .await?;
400
401 Ok(AdapterVersion {
402 tag_name: release.tag_name,
403 url: release.zipball_url,
404 })
405}
406
407pub trait InlineValueProvider {
408 fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue>;
409}
410
411#[async_trait(?Send)]
412pub trait DebugAdapter: 'static + Send + Sync {
413 fn name(&self) -> DebugAdapterName;
414
415 async fn get_binary(
416 &self,
417 delegate: &dyn DapDelegate,
418 config: &DebugTaskDefinition,
419 user_installed_path: Option<PathBuf>,
420 cx: &mut AsyncApp,
421 ) -> Result<DebugAdapterBinary> {
422 if delegate
423 .updated_adapters()
424 .lock()
425 .await
426 .contains(&self.name())
427 {
428 log::info!("Using cached debug adapter binary {}", self.name());
429
430 if let Some(binary) = self
431 .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
432 .await
433 .log_err()
434 {
435 return Ok(binary);
436 }
437
438 log::info!(
439 "Cached binary {} is corrupt falling back to install",
440 self.name()
441 );
442 }
443
444 log::info!("Getting latest version of debug adapter {}", self.name());
445 delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
446 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
447 log::info!("Installing latest version of debug adapter {}", self.name());
448 delegate.update_status(self.name(), DapStatus::Downloading);
449 match self.install_binary(version, delegate).await {
450 Ok(_) => {
451 delegate.update_status(self.name(), DapStatus::None);
452 }
453 Err(error) => {
454 delegate.update_status(
455 self.name(),
456 DapStatus::Failed {
457 error: error.to_string(),
458 },
459 );
460
461 return Err(error);
462 }
463 }
464
465 delegate
466 .updated_adapters()
467 .lock_arc()
468 .await
469 .insert(self.name());
470 }
471
472 self.get_installed_binary(delegate, &config, user_installed_path, cx)
473 .await
474 }
475
476 async fn fetch_latest_adapter_version(
477 &self,
478 delegate: &dyn DapDelegate,
479 ) -> Result<AdapterVersion>;
480
481 /// Installs the binary for the debug adapter.
482 /// This method is called when the adapter binary is not found or needs to be updated.
483 /// It should download and install the necessary files for the debug adapter to function.
484 async fn install_binary(
485 &self,
486 version: AdapterVersion,
487 delegate: &dyn DapDelegate,
488 ) -> Result<()>;
489
490 async fn get_installed_binary(
491 &self,
492 delegate: &dyn DapDelegate,
493 config: &DebugTaskDefinition,
494 user_installed_path: Option<PathBuf>,
495 cx: &mut AsyncApp,
496 ) -> Result<DebugAdapterBinary>;
497
498 fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
499 None
500 }
501}
502
503#[cfg(any(test, feature = "test-support"))]
504pub struct FakeAdapter {}
505
506#[cfg(any(test, feature = "test-support"))]
507impl FakeAdapter {
508 pub const ADAPTER_NAME: &'static str = "fake-adapter";
509
510 pub fn new() -> Self {
511 Self {}
512 }
513
514 fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
515 use serde_json::json;
516 use task::DebugRequest;
517
518 let value = json!({
519 "request": match config.request {
520 DebugRequest::Launch(_) => "launch",
521 DebugRequest::Attach(_) => "attach",
522 },
523 "process_id": if let DebugRequest::Attach(attach_config) = &config.request {
524 attach_config.process_id
525 } else {
526 None
527 },
528 "raw_request": serde_json::to_value(config).unwrap()
529 });
530 let request = match config.request {
531 DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
532 DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
533 };
534 StartDebuggingRequestArguments {
535 configuration: value,
536 request,
537 }
538 }
539}
540
541#[cfg(any(test, feature = "test-support"))]
542#[async_trait(?Send)]
543impl DebugAdapter for FakeAdapter {
544 fn name(&self) -> DebugAdapterName {
545 DebugAdapterName(Self::ADAPTER_NAME.into())
546 }
547
548 async fn get_binary(
549 &self,
550 _: &dyn DapDelegate,
551 config: &DebugTaskDefinition,
552 _: Option<PathBuf>,
553 _: &mut AsyncApp,
554 ) -> Result<DebugAdapterBinary> {
555 Ok(DebugAdapterBinary {
556 command: "command".into(),
557 arguments: vec![],
558 connection: None,
559 envs: HashMap::default(),
560 cwd: None,
561 request_args: self.request_args(config),
562 })
563 }
564
565 async fn fetch_latest_adapter_version(
566 &self,
567 _delegate: &dyn DapDelegate,
568 ) -> Result<AdapterVersion> {
569 unimplemented!("fetch latest adapter version");
570 }
571
572 async fn install_binary(
573 &self,
574 _version: AdapterVersion,
575 _delegate: &dyn DapDelegate,
576 ) -> Result<()> {
577 unimplemented!("install binary");
578 }
579
580 async fn get_installed_binary(
581 &self,
582 _: &dyn DapDelegate,
583 _: &DebugTaskDefinition,
584 _: Option<PathBuf>,
585 _: &mut AsyncApp,
586 ) -> Result<DebugAdapterBinary> {
587 unimplemented!("get installed binary");
588 }
589}