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