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