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