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