adapters.rs

  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}