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,
 18    collections::HashSet,
 19    ffi::OsStr,
 20    fmt::Debug,
 21    net::Ipv4Addr,
 22    ops::Deref,
 23    path::{Path, PathBuf},
 24    sync::Arc,
 25};
 26use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
 27use util::ResultExt;
 28
 29#[derive(Clone, Debug, PartialEq, Eq)]
 30pub enum DapStatus {
 31    None,
 32    CheckingForUpdate,
 33    Downloading,
 34    Failed { error: String },
 35}
 36
 37#[async_trait(?Send)]
 38pub trait DapDelegate {
 39    fn worktree_id(&self) -> WorktreeId;
 40    fn http_client(&self) -> Arc<dyn HttpClient>;
 41    fn node_runtime(&self) -> NodeRuntime;
 42    fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
 43    fn fs(&self) -> Arc<dyn Fs>;
 44    fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
 45    fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
 46    fn which(&self, command: &OsStr) -> Option<PathBuf>;
 47    async fn shell_env(&self) -> collections::HashMap<String, String>;
 48}
 49
 50#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
 51pub struct DebugAdapterName(pub SharedString);
 52
 53impl Deref for DebugAdapterName {
 54    type Target = str;
 55
 56    fn deref(&self) -> &Self::Target {
 57        &self.0
 58    }
 59}
 60
 61impl AsRef<str> for DebugAdapterName {
 62    fn as_ref(&self) -> &str {
 63        &self.0
 64    }
 65}
 66
 67impl Borrow<str> for DebugAdapterName {
 68    fn borrow(&self) -> &str {
 69        &self.0
 70    }
 71}
 72
 73impl std::fmt::Display for DebugAdapterName {
 74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 75        std::fmt::Display::fmt(&self.0, f)
 76    }
 77}
 78
 79impl From<DebugAdapterName> for SharedString {
 80    fn from(name: DebugAdapterName) -> Self {
 81        name.0
 82    }
 83}
 84
 85impl<'a> From<&'a str> for DebugAdapterName {
 86    fn from(str: &'a str) -> DebugAdapterName {
 87        DebugAdapterName(str.to_string().into())
 88    }
 89}
 90
 91#[derive(Debug, Clone, PartialEq)]
 92pub struct TcpArguments {
 93    pub host: Ipv4Addr,
 94    pub port: u16,
 95    pub timeout: Option<u64>,
 96}
 97
 98impl TcpArguments {
 99    pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
100        let host = TcpArgumentsTemplate::from_proto(proto)?;
101        Ok(TcpArguments {
102            host: host.host.ok_or_else(|| anyhow!("missing host"))?,
103            port: host.port.ok_or_else(|| anyhow!("missing port"))?,
104            timeout: host.timeout,
105        })
106    }
107
108    pub fn to_proto(&self) -> proto::TcpHost {
109        TcpArgumentsTemplate {
110            host: Some(self.host),
111            port: Some(self.port),
112            timeout: self.timeout,
113        }
114        .to_proto()
115    }
116}
117
118/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
119///
120/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
121/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
122/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
123#[derive(Clone, Debug, PartialEq)]
124#[cfg_attr(
125    any(feature = "test-support", test),
126    derive(serde::Deserialize, serde::Serialize)
127)]
128pub struct DebugTaskDefinition {
129    pub label: SharedString,
130    pub adapter: DebugAdapterName,
131    pub request: DebugRequest,
132    /// Additional initialization arguments to be sent on DAP initialization
133    pub initialize_args: Option<serde_json::Value>,
134    /// Whether to tell the debug adapter to stop on entry
135    pub stop_on_entry: Option<bool>,
136    /// Optional TCP connection information
137    ///
138    /// If provided, this will be used to connect to the debug adapter instead of
139    /// spawning a new debug adapter process. This is useful for connecting to a debug adapter
140    /// that is already running or is started by another process.
141    pub tcp_connection: Option<TcpArgumentsTemplate>,
142}
143
144impl DebugTaskDefinition {
145    pub fn cwd(&self) -> Option<&Path> {
146        if let DebugRequest::Launch(config) = &self.request {
147            config.cwd.as_ref().map(Path::new)
148        } else {
149            None
150        }
151    }
152
153    pub fn to_scenario(&self) -> DebugScenario {
154        DebugScenario {
155            label: self.label.clone(),
156            adapter: self.adapter.clone().into(),
157            build: None,
158            request: Some(self.request.clone()),
159            stop_on_entry: self.stop_on_entry,
160            tcp_connection: self.tcp_connection.clone(),
161            initialize_args: self.initialize_args.clone(),
162        }
163    }
164
165    pub fn to_proto(&self) -> proto::DebugTaskDefinition {
166        proto::DebugTaskDefinition {
167            adapter: self.adapter.to_string(),
168            request: Some(match &self.request {
169                DebugRequest::Launch(config) => {
170                    proto::debug_task_definition::Request::DebugLaunchRequest(
171                        proto::DebugLaunchRequest {
172                            program: config.program.clone(),
173                            cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
174                            args: config.args.clone(),
175                            env: config
176                                .env
177                                .iter()
178                                .map(|(k, v)| (k.clone(), v.clone()))
179                                .collect(),
180                        },
181                    )
182                }
183                DebugRequest::Attach(attach_request) => {
184                    proto::debug_task_definition::Request::DebugAttachRequest(
185                        proto::DebugAttachRequest {
186                            process_id: attach_request.process_id.unwrap_or_default(),
187                        },
188                    )
189                }
190            }),
191            label: self.label.to_string(),
192            initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
193            tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
194            stop_on_entry: self.stop_on_entry,
195        }
196    }
197
198    pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
199        let request = proto
200            .request
201            .ok_or_else(|| anyhow::anyhow!("request is required"))?;
202        Ok(Self {
203            label: proto.label.into(),
204            initialize_args: proto.initialize_args.map(|v| v.into()),
205            tcp_connection: proto
206                .tcp_connection
207                .map(TcpArgumentsTemplate::from_proto)
208                .transpose()?,
209            stop_on_entry: proto.stop_on_entry,
210            adapter: DebugAdapterName(proto.adapter.into()),
211            request: match request {
212                proto::debug_task_definition::Request::DebugAttachRequest(config) => {
213                    DebugRequest::Attach(AttachRequest {
214                        process_id: Some(config.process_id),
215                    })
216                }
217
218                proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
219                    DebugRequest::Launch(LaunchRequest {
220                        program: config.program,
221                        cwd: config.cwd.map(|cwd| cwd.into()),
222                        args: config.args,
223                        env: Default::default(),
224                    })
225                }
226            },
227        })
228    }
229}
230
231/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
232#[derive(Debug, Clone, PartialEq)]
233pub struct DebugAdapterBinary {
234    pub command: String,
235    pub arguments: Vec<String>,
236    pub envs: HashMap<String, String>,
237    pub cwd: Option<PathBuf>,
238    pub connection: Option<TcpArguments>,
239    pub request_args: StartDebuggingRequestArguments,
240}
241
242impl DebugAdapterBinary {
243    pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
244        let request = match binary.launch_type() {
245            proto::debug_adapter_binary::LaunchType::Launch => {
246                StartDebuggingRequestArgumentsRequest::Launch
247            }
248            proto::debug_adapter_binary::LaunchType::Attach => {
249                StartDebuggingRequestArgumentsRequest::Attach
250            }
251        };
252
253        Ok(DebugAdapterBinary {
254            command: binary.command,
255            arguments: binary.arguments,
256            envs: binary.envs.into_iter().collect(),
257            connection: binary
258                .connection
259                .map(TcpArguments::from_proto)
260                .transpose()?,
261            request_args: StartDebuggingRequestArguments {
262                configuration: serde_json::from_str(&binary.configuration)?,
263                request,
264            },
265            cwd: binary.cwd.map(|cwd| cwd.into()),
266        })
267    }
268
269    pub fn to_proto(&self) -> proto::DebugAdapterBinary {
270        proto::DebugAdapterBinary {
271            command: self.command.clone(),
272            arguments: self.arguments.clone(),
273            envs: self
274                .envs
275                .iter()
276                .map(|(k, v)| (k.clone(), v.clone()))
277                .collect(),
278            cwd: self
279                .cwd
280                .as_ref()
281                .map(|cwd| cwd.to_string_lossy().to_string()),
282            connection: self.connection.as_ref().map(|c| c.to_proto()),
283            launch_type: match self.request_args.request {
284                StartDebuggingRequestArgumentsRequest::Launch => {
285                    proto::debug_adapter_binary::LaunchType::Launch.into()
286                }
287                StartDebuggingRequestArgumentsRequest::Attach => {
288                    proto::debug_adapter_binary::LaunchType::Attach.into()
289                }
290            },
291            configuration: self.request_args.configuration.to_string(),
292        }
293    }
294}
295
296#[derive(Debug)]
297pub struct AdapterVersion {
298    pub tag_name: String,
299    pub url: String,
300}
301
302pub enum DownloadedFileType {
303    Vsix,
304    GzipTar,
305    Zip,
306}
307
308pub struct GithubRepo {
309    pub repo_name: String,
310    pub repo_owner: String,
311}
312
313pub async fn download_adapter_from_github(
314    adapter_name: DebugAdapterName,
315    github_version: AdapterVersion,
316    file_type: DownloadedFileType,
317    delegate: &dyn DapDelegate,
318) -> Result<PathBuf> {
319    let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
320    let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
321    let fs = delegate.fs();
322
323    if version_path.exists() {
324        return Ok(version_path);
325    }
326
327    if !adapter_path.exists() {
328        fs.create_dir(&adapter_path.as_path())
329            .await
330            .context("Failed creating adapter path")?;
331    }
332
333    log::debug!(
334        "Downloading adapter {} from {}",
335        adapter_name,
336        &github_version.url,
337    );
338
339    let mut response = delegate
340        .http_client()
341        .get(&github_version.url, Default::default(), true)
342        .await
343        .context("Error downloading release")?;
344    if !response.status().is_success() {
345        Err(anyhow!(
346            "download failed with status {}",
347            response.status().to_string()
348        ))?;
349    }
350
351    match file_type {
352        DownloadedFileType::GzipTar => {
353            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
354            let archive = Archive::new(decompressed_bytes);
355            archive.unpack(&version_path).await?;
356        }
357        DownloadedFileType::Zip | DownloadedFileType::Vsix => {
358            let zip_path = version_path.with_extension("zip");
359
360            let mut file = File::create(&zip_path).await?;
361            futures::io::copy(response.body_mut(), &mut file).await?;
362
363            // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
364            util::command::new_smol_command("unzip")
365                .arg(&zip_path)
366                .arg("-d")
367                .arg(&version_path)
368                .output()
369                .await?;
370
371            util::fs::remove_matching(&adapter_path, |entry| {
372                entry
373                    .file_name()
374                    .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
375            })
376            .await;
377        }
378    }
379
380    // remove older versions
381    util::fs::remove_matching(&adapter_path, |entry| {
382        entry.to_string_lossy() != version_path.to_string_lossy()
383    })
384    .await;
385
386    Ok(version_path)
387}
388
389pub async fn fetch_latest_adapter_version_from_github(
390    github_repo: GithubRepo,
391    delegate: &dyn DapDelegate,
392) -> Result<AdapterVersion> {
393    let release = latest_github_release(
394        &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
395        false,
396        false,
397        delegate.http_client(),
398    )
399    .await?;
400
401    Ok(AdapterVersion {
402        tag_name: release.tag_name,
403        url: release.zipball_url,
404    })
405}
406
407pub trait InlineValueProvider {
408    fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue>;
409}
410
411#[async_trait(?Send)]
412pub trait DebugAdapter: 'static + Send + Sync {
413    fn name(&self) -> DebugAdapterName;
414
415    async fn get_binary(
416        &self,
417        delegate: &dyn DapDelegate,
418        config: &DebugTaskDefinition,
419        user_installed_path: Option<PathBuf>,
420        cx: &mut AsyncApp,
421    ) -> Result<DebugAdapterBinary> {
422        if delegate
423            .updated_adapters()
424            .lock()
425            .await
426            .contains(&self.name())
427        {
428            log::info!("Using cached debug adapter binary {}", self.name());
429
430            if let Some(binary) = self
431                .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
432                .await
433                .log_err()
434            {
435                return Ok(binary);
436            }
437
438            log::info!(
439                "Cached binary {} is corrupt falling back to install",
440                self.name()
441            );
442        }
443
444        log::info!("Getting latest version of debug adapter {}", self.name());
445        delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
446        if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
447            log::info!("Installing latest version of debug adapter {}", self.name());
448            delegate.update_status(self.name(), DapStatus::Downloading);
449            match self.install_binary(version, delegate).await {
450                Ok(_) => {
451                    delegate.update_status(self.name(), DapStatus::None);
452                }
453                Err(error) => {
454                    delegate.update_status(
455                        self.name(),
456                        DapStatus::Failed {
457                            error: error.to_string(),
458                        },
459                    );
460
461                    return Err(error);
462                }
463            }
464
465            delegate
466                .updated_adapters()
467                .lock_arc()
468                .await
469                .insert(self.name());
470        }
471
472        self.get_installed_binary(delegate, &config, user_installed_path, cx)
473            .await
474    }
475
476    async fn fetch_latest_adapter_version(
477        &self,
478        delegate: &dyn DapDelegate,
479    ) -> Result<AdapterVersion>;
480
481    /// Installs the binary for the debug adapter.
482    /// This method is called when the adapter binary is not found or needs to be updated.
483    /// It should download and install the necessary files for the debug adapter to function.
484    async fn install_binary(
485        &self,
486        version: AdapterVersion,
487        delegate: &dyn DapDelegate,
488    ) -> Result<()>;
489
490    async fn get_installed_binary(
491        &self,
492        delegate: &dyn DapDelegate,
493        config: &DebugTaskDefinition,
494        user_installed_path: Option<PathBuf>,
495        cx: &mut AsyncApp,
496    ) -> Result<DebugAdapterBinary>;
497
498    fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
499        None
500    }
501}
502
503#[cfg(any(test, feature = "test-support"))]
504pub struct FakeAdapter {}
505
506#[cfg(any(test, feature = "test-support"))]
507impl FakeAdapter {
508    pub const ADAPTER_NAME: &'static str = "fake-adapter";
509
510    pub fn new() -> Self {
511        Self {}
512    }
513
514    fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
515        use serde_json::json;
516        use task::DebugRequest;
517
518        let value = json!({
519            "request": match config.request {
520                DebugRequest::Launch(_) => "launch",
521                DebugRequest::Attach(_) => "attach",
522            },
523            "process_id": if let DebugRequest::Attach(attach_config) = &config.request {
524                attach_config.process_id
525            } else {
526                None
527            },
528            "raw_request": serde_json::to_value(config).unwrap()
529        });
530        let request = match config.request {
531            DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
532            DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
533        };
534        StartDebuggingRequestArguments {
535            configuration: value,
536            request,
537        }
538    }
539}
540
541#[cfg(any(test, feature = "test-support"))]
542#[async_trait(?Send)]
543impl DebugAdapter for FakeAdapter {
544    fn name(&self) -> DebugAdapterName {
545        DebugAdapterName(Self::ADAPTER_NAME.into())
546    }
547
548    async fn get_binary(
549        &self,
550        _: &dyn DapDelegate,
551        config: &DebugTaskDefinition,
552        _: Option<PathBuf>,
553        _: &mut AsyncApp,
554    ) -> Result<DebugAdapterBinary> {
555        Ok(DebugAdapterBinary {
556            command: "command".into(),
557            arguments: vec![],
558            connection: None,
559            envs: HashMap::default(),
560            cwd: None,
561            request_args: self.request_args(config),
562        })
563    }
564
565    async fn fetch_latest_adapter_version(
566        &self,
567        _delegate: &dyn DapDelegate,
568    ) -> Result<AdapterVersion> {
569        unimplemented!("fetch latest adapter version");
570    }
571
572    async fn install_binary(
573        &self,
574        _version: AdapterVersion,
575        _delegate: &dyn DapDelegate,
576    ) -> Result<()> {
577        unimplemented!("install binary");
578    }
579
580    async fn get_installed_binary(
581        &self,
582        _: &dyn DapDelegate,
583        _: &DebugTaskDefinition,
584        _: Option<PathBuf>,
585        _: &mut AsyncApp,
586    ) -> Result<DebugAdapterBinary> {
587        unimplemented!("get installed binary");
588    }
589}