adapters.rs

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