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