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