1use ::fs::Fs;
2use anyhow::{Context as _, Ok, Result, anyhow};
3use async_compression::futures::bufread::GzipDecoder;
4use async_tar::Archive;
5use async_trait::async_trait;
6use futures::io::BufReader;
7use gpui::{AsyncApp, SharedString};
8pub use http_client::{HttpClient, github::latest_github_release};
9use language::LanguageToolchainStore;
10use node_runtime::NodeRuntime;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use settings::WorktreeId;
14use smol::{self, fs::File, lock::Mutex};
15use std::{
16 borrow::Borrow,
17 collections::{HashMap, HashSet},
18 ffi::{OsStr, OsString},
19 fmt::Debug,
20 net::Ipv4Addr,
21 ops::Deref,
22 path::PathBuf,
23 sync::{Arc, LazyLock},
24};
25use task::{DebugAdapterConfig, DebugTaskDefinition};
26use util::ResultExt;
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(?Send)]
37pub trait DapDelegate {
38 fn worktree_id(&self) -> WorktreeId;
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 updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
44 fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
45 fn which(&self, command: &OsStr) -> Option<PathBuf>;
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}
83
84impl<'a> From<&'a str> for DebugAdapterName {
85 fn from(str: &'a str) -> DebugAdapterName {
86 DebugAdapterName(str.to_string().into())
87 }
88}
89
90#[derive(Debug, Clone)]
91pub struct TcpArguments {
92 pub host: Ipv4Addr,
93 pub port: u16,
94 pub timeout: Option<u64>,
95}
96#[derive(Debug, Clone)]
97pub struct DebugAdapterBinary {
98 pub command: String,
99 pub arguments: Option<Vec<OsString>>,
100 pub envs: Option<HashMap<String, String>>,
101 pub cwd: Option<PathBuf>,
102 pub connection: Option<TcpArguments>,
103}
104
105pub struct AdapterVersion {
106 pub tag_name: String,
107 pub url: String,
108}
109
110pub enum DownloadedFileType {
111 Vsix,
112 GzipTar,
113 Zip,
114}
115
116pub struct GithubRepo {
117 pub repo_name: String,
118 pub repo_owner: String,
119}
120
121pub async fn download_adapter_from_github(
122 adapter_name: DebugAdapterName,
123 github_version: AdapterVersion,
124 file_type: DownloadedFileType,
125 delegate: &dyn DapDelegate,
126) -> Result<PathBuf> {
127 let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
128 let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
129 let fs = delegate.fs();
130
131 if version_path.exists() {
132 return Ok(version_path);
133 }
134
135 if !adapter_path.exists() {
136 fs.create_dir(&adapter_path.as_path())
137 .await
138 .context("Failed creating adapter path")?;
139 }
140
141 log::debug!(
142 "Downloading adapter {} from {}",
143 adapter_name,
144 &github_version.url,
145 );
146
147 let mut response = delegate
148 .http_client()
149 .get(&github_version.url, Default::default(), true)
150 .await
151 .context("Error downloading release")?;
152 if !response.status().is_success() {
153 Err(anyhow!(
154 "download failed with status {}",
155 response.status().to_string()
156 ))?;
157 }
158
159 match file_type {
160 DownloadedFileType::GzipTar => {
161 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
162 let archive = Archive::new(decompressed_bytes);
163 archive.unpack(&version_path).await?;
164 }
165 DownloadedFileType::Zip | DownloadedFileType::Vsix => {
166 let zip_path = version_path.with_extension("zip");
167
168 let mut file = File::create(&zip_path).await?;
169 futures::io::copy(response.body_mut(), &mut file).await?;
170
171 // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
172 util::command::new_smol_command("unzip")
173 .arg(&zip_path)
174 .arg("-d")
175 .arg(&version_path)
176 .output()
177 .await?;
178
179 util::fs::remove_matching(&adapter_path, |entry| {
180 entry
181 .file_name()
182 .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
183 })
184 .await;
185 }
186 }
187
188 // remove older versions
189 util::fs::remove_matching(&adapter_path, |entry| {
190 entry.to_string_lossy() != version_path.to_string_lossy()
191 })
192 .await;
193
194 Ok(version_path)
195}
196
197pub async fn fetch_latest_adapter_version_from_github(
198 github_repo: GithubRepo,
199 delegate: &dyn DapDelegate,
200) -> Result<AdapterVersion> {
201 let release = latest_github_release(
202 &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
203 false,
204 false,
205 delegate.http_client(),
206 )
207 .await?;
208
209 Ok(AdapterVersion {
210 tag_name: release.tag_name,
211 url: release.zipball_url,
212 })
213}
214
215#[async_trait(?Send)]
216pub trait DebugAdapter: 'static + Send + Sync {
217 fn name(&self) -> DebugAdapterName;
218
219 async fn get_binary(
220 &self,
221 delegate: &dyn DapDelegate,
222 config: &DebugAdapterConfig,
223 user_installed_path: Option<PathBuf>,
224 cx: &mut AsyncApp,
225 ) -> Result<DebugAdapterBinary> {
226 if delegate
227 .updated_adapters()
228 .lock()
229 .await
230 .contains(&self.name())
231 {
232 log::info!("Using cached debug adapter binary {}", self.name());
233
234 if let Some(binary) = self
235 .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
236 .await
237 .log_err()
238 {
239 return Ok(binary);
240 }
241
242 log::info!(
243 "Cached binary {} is corrupt falling back to install",
244 self.name()
245 );
246 }
247
248 log::info!("Getting latest version of debug adapter {}", self.name());
249 delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
250 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
251 log::info!(
252 "Installiing latest version of debug adapter {}",
253 self.name()
254 );
255 delegate.update_status(self.name(), DapStatus::Downloading);
256 self.install_binary(version, delegate).await?;
257
258 delegate
259 .updated_adapters()
260 .lock_arc()
261 .await
262 .insert(self.name());
263 }
264
265 self.get_installed_binary(delegate, &config, user_installed_path, cx)
266 .await
267 }
268
269 async fn fetch_latest_adapter_version(
270 &self,
271 delegate: &dyn DapDelegate,
272 ) -> Result<AdapterVersion>;
273
274 /// Installs the binary for the debug adapter.
275 /// This method is called when the adapter binary is not found or needs to be updated.
276 /// It should download and install the necessary files for the debug adapter to function.
277 async fn install_binary(
278 &self,
279 version: AdapterVersion,
280 delegate: &dyn DapDelegate,
281 ) -> Result<()>;
282
283 async fn get_installed_binary(
284 &self,
285 delegate: &dyn DapDelegate,
286 config: &DebugAdapterConfig,
287 user_installed_path: Option<PathBuf>,
288 cx: &mut AsyncApp,
289 ) -> Result<DebugAdapterBinary>;
290
291 /// Should return base configuration to make the debug adapter work
292 fn request_args(&self, config: &DebugTaskDefinition) -> Value;
293
294 fn attach_processes_filter(&self) -> regex::Regex {
295 EMPTY_REGEX.clone()
296 }
297}
298
299static EMPTY_REGEX: LazyLock<regex::Regex> =
300 LazyLock::new(|| regex::Regex::new("").expect("Regex compilation to succeed"));
301#[cfg(any(test, feature = "test-support"))]
302pub struct FakeAdapter {}
303
304#[cfg(any(test, feature = "test-support"))]
305impl FakeAdapter {
306 pub const ADAPTER_NAME: &'static str = "fake-adapter";
307
308 pub fn new() -> Self {
309 Self {}
310 }
311}
312
313#[cfg(any(test, feature = "test-support"))]
314#[async_trait(?Send)]
315impl DebugAdapter for FakeAdapter {
316 fn name(&self) -> DebugAdapterName {
317 DebugAdapterName(Self::ADAPTER_NAME.into())
318 }
319
320 async fn get_binary(
321 &self,
322 _: &dyn DapDelegate,
323 _: &DebugAdapterConfig,
324 _: Option<PathBuf>,
325 _: &mut AsyncApp,
326 ) -> Result<DebugAdapterBinary> {
327 Ok(DebugAdapterBinary {
328 command: "command".into(),
329 arguments: None,
330 connection: None,
331 envs: None,
332 cwd: None,
333 })
334 }
335
336 async fn fetch_latest_adapter_version(
337 &self,
338 _delegate: &dyn DapDelegate,
339 ) -> Result<AdapterVersion> {
340 unimplemented!("fetch latest adapter version");
341 }
342
343 async fn install_binary(
344 &self,
345 _version: AdapterVersion,
346 _delegate: &dyn DapDelegate,
347 ) -> Result<()> {
348 unimplemented!("install binary");
349 }
350
351 async fn get_installed_binary(
352 &self,
353 _: &dyn DapDelegate,
354 _: &DebugAdapterConfig,
355 _: Option<PathBuf>,
356 _: &mut AsyncApp,
357 ) -> Result<DebugAdapterBinary> {
358 unimplemented!("get installed binary");
359 }
360
361 fn request_args(&self, config: &DebugTaskDefinition) -> Value {
362 use serde_json::json;
363 use task::DebugRequestType;
364
365 json!({
366 "request": match config.request {
367 DebugRequestType::Launch(_) => "launch",
368 DebugRequestType::Attach(_) => "attach",
369 },
370 "process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
371 attach_config.process_id
372 } else {
373 None
374 },
375 })
376 }
377
378 fn attach_processes_filter(&self) -> regex::Regex {
379 static REGEX: LazyLock<regex::Regex> =
380 LazyLock::new(|| regex::Regex::new("^fake-binary").unwrap());
381 REGEX.clone()
382 }
383}