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