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