adapters.rs

  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}