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