adapters.rs

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