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
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}