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