adapters.rs

  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}