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
287#[async_trait(?Send)]
288pub trait DebugAdapter: 'static + Send + Sync {
289 fn name(&self) -> DebugAdapterName;
290
291 async fn get_binary(
292 &self,
293 delegate: &dyn DapDelegate,
294 config: &DebugTaskDefinition,
295 user_installed_path: Option<PathBuf>,
296 cx: &mut AsyncApp,
297 ) -> Result<DebugAdapterBinary> {
298 if delegate
299 .updated_adapters()
300 .lock()
301 .await
302 .contains(&self.name())
303 {
304 log::info!("Using cached debug adapter binary {}", self.name());
305
306 if let Some(binary) = self
307 .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
308 .await
309 .log_err()
310 {
311 return Ok(binary);
312 }
313
314 log::info!(
315 "Cached binary {} is corrupt falling back to install",
316 self.name()
317 );
318 }
319
320 log::info!("Getting latest version of debug adapter {}", self.name());
321 delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
322 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
323 log::info!(
324 "Installiing latest version of debug adapter {}",
325 self.name()
326 );
327 delegate.update_status(self.name(), DapStatus::Downloading);
328 match self.install_binary(version, delegate).await {
329 Ok(_) => {
330 delegate.update_status(self.name(), DapStatus::None);
331 }
332 Err(error) => {
333 delegate.update_status(
334 self.name(),
335 DapStatus::Failed {
336 error: error.to_string(),
337 },
338 );
339
340 return Err(error);
341 }
342 }
343
344 delegate
345 .updated_adapters()
346 .lock_arc()
347 .await
348 .insert(self.name());
349 }
350
351 self.get_installed_binary(delegate, &config, user_installed_path, cx)
352 .await
353 }
354
355 async fn fetch_latest_adapter_version(
356 &self,
357 delegate: &dyn DapDelegate,
358 ) -> Result<AdapterVersion>;
359
360 /// Installs the binary for the debug adapter.
361 /// This method is called when the adapter binary is not found or needs to be updated.
362 /// It should download and install the necessary files for the debug adapter to function.
363 async fn install_binary(
364 &self,
365 version: AdapterVersion,
366 delegate: &dyn DapDelegate,
367 ) -> Result<()>;
368
369 async fn get_installed_binary(
370 &self,
371 delegate: &dyn DapDelegate,
372 config: &DebugTaskDefinition,
373 user_installed_path: Option<PathBuf>,
374 cx: &mut AsyncApp,
375 ) -> Result<DebugAdapterBinary>;
376}
377#[cfg(any(test, feature = "test-support"))]
378pub struct FakeAdapter {}
379
380#[cfg(any(test, feature = "test-support"))]
381impl FakeAdapter {
382 pub const ADAPTER_NAME: &'static str = "fake-adapter";
383
384 pub fn new() -> Self {
385 Self {}
386 }
387
388 fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
389 use serde_json::json;
390 use task::DebugRequest;
391
392 let value = json!({
393 "request": match config.request {
394 DebugRequest::Launch(_) => "launch",
395 DebugRequest::Attach(_) => "attach",
396 },
397 "process_id": if let DebugRequest::Attach(attach_config) = &config.request {
398 attach_config.process_id
399 } else {
400 None
401 },
402 });
403 let request = match config.request {
404 DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
405 DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
406 };
407 StartDebuggingRequestArguments {
408 configuration: value,
409 request,
410 }
411 }
412}
413
414#[cfg(any(test, feature = "test-support"))]
415#[async_trait(?Send)]
416impl DebugAdapter for FakeAdapter {
417 fn name(&self) -> DebugAdapterName {
418 DebugAdapterName(Self::ADAPTER_NAME.into())
419 }
420
421 async fn get_binary(
422 &self,
423 _: &dyn DapDelegate,
424 config: &DebugTaskDefinition,
425 _: Option<PathBuf>,
426 _: &mut AsyncApp,
427 ) -> Result<DebugAdapterBinary> {
428 Ok(DebugAdapterBinary {
429 command: "command".into(),
430 arguments: vec![],
431 connection: None,
432 envs: HashMap::default(),
433 cwd: None,
434 request_args: self.request_args(config),
435 })
436 }
437
438 async fn fetch_latest_adapter_version(
439 &self,
440 _delegate: &dyn DapDelegate,
441 ) -> Result<AdapterVersion> {
442 unimplemented!("fetch latest adapter version");
443 }
444
445 async fn install_binary(
446 &self,
447 _version: AdapterVersion,
448 _delegate: &dyn DapDelegate,
449 ) -> Result<()> {
450 unimplemented!("install binary");
451 }
452
453 async fn get_installed_binary(
454 &self,
455 _: &dyn DapDelegate,
456 _: &DebugTaskDefinition,
457 _: Option<PathBuf>,
458 _: &mut AsyncApp,
459 ) -> Result<DebugAdapterBinary> {
460 unimplemented!("get installed binary");
461 }
462}