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