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