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