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::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::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
 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    /// The name of this debug task
135    pub label: SharedString,
136    /// The debug adapter to use
137    pub adapter: DebugAdapterName,
138    /// The configuration to send to the debug adapter
139    pub config: serde_json::Value,
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 to_scenario(&self) -> DebugScenario {
150        DebugScenario {
151            label: self.label.clone(),
152            adapter: self.adapter.clone().into(),
153            build: None,
154            tcp_connection: self.tcp_connection.clone(),
155            config: self.config.clone(),
156        }
157    }
158
159    pub fn to_proto(&self) -> proto::DebugTaskDefinition {
160        proto::DebugTaskDefinition {
161            label: self.label.clone().into(),
162            config: self.config.to_string(),
163            tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
164            adapter: self.adapter.clone().0.into(),
165        }
166    }
167
168    pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
169        Ok(Self {
170            label: proto.label.into(),
171            config: serde_json::from_str(&proto.config)?,
172            tcp_connection: proto
173                .tcp_connection
174                .map(TcpArgumentsTemplate::from_proto)
175                .transpose()?,
176            adapter: DebugAdapterName(proto.adapter.into()),
177        })
178    }
179}
180
181/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
182#[derive(Debug, Clone, PartialEq)]
183pub struct DebugAdapterBinary {
184    pub command: String,
185    pub arguments: Vec<String>,
186    pub envs: HashMap<String, String>,
187    pub cwd: Option<PathBuf>,
188    pub connection: Option<TcpArguments>,
189    pub request_args: StartDebuggingRequestArguments,
190}
191
192impl DebugAdapterBinary {
193    pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
194        let request = match binary.launch_type() {
195            proto::debug_adapter_binary::LaunchType::Launch => {
196                StartDebuggingRequestArgumentsRequest::Launch
197            }
198            proto::debug_adapter_binary::LaunchType::Attach => {
199                StartDebuggingRequestArgumentsRequest::Attach
200            }
201        };
202
203        Ok(DebugAdapterBinary {
204            command: binary.command,
205            arguments: binary.arguments,
206            envs: binary.envs.into_iter().collect(),
207            connection: binary
208                .connection
209                .map(TcpArguments::from_proto)
210                .transpose()?,
211            request_args: StartDebuggingRequestArguments {
212                configuration: serde_json::from_str(&binary.configuration)?,
213                request,
214            },
215            cwd: binary.cwd.map(|cwd| cwd.into()),
216        })
217    }
218
219    pub fn to_proto(&self) -> proto::DebugAdapterBinary {
220        proto::DebugAdapterBinary {
221            command: self.command.clone(),
222            arguments: self.arguments.clone(),
223            envs: self
224                .envs
225                .iter()
226                .map(|(k, v)| (k.clone(), v.clone()))
227                .collect(),
228            cwd: self
229                .cwd
230                .as_ref()
231                .map(|cwd| cwd.to_string_lossy().to_string()),
232            connection: self.connection.as_ref().map(|c| c.to_proto()),
233            launch_type: match self.request_args.request {
234                StartDebuggingRequestArgumentsRequest::Launch => {
235                    proto::debug_adapter_binary::LaunchType::Launch.into()
236                }
237                StartDebuggingRequestArgumentsRequest::Attach => {
238                    proto::debug_adapter_binary::LaunchType::Attach.into()
239                }
240            },
241            configuration: self.request_args.configuration.to_string(),
242        }
243    }
244}
245
246#[derive(Debug, Clone)]
247pub struct AdapterVersion {
248    pub tag_name: String,
249    pub url: String,
250}
251
252pub enum DownloadedFileType {
253    Vsix,
254    GzipTar,
255    Zip,
256}
257
258pub struct GithubRepo {
259    pub repo_name: String,
260    pub repo_owner: String,
261}
262
263pub async fn download_adapter_from_github(
264    adapter_name: DebugAdapterName,
265    github_version: AdapterVersion,
266    file_type: DownloadedFileType,
267    delegate: &dyn DapDelegate,
268) -> Result<PathBuf> {
269    let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
270    let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
271    let fs = delegate.fs();
272
273    if version_path.exists() {
274        return Ok(version_path);
275    }
276
277    if !adapter_path.exists() {
278        fs.create_dir(&adapter_path.as_path())
279            .await
280            .context("Failed creating adapter path")?;
281    }
282
283    log::debug!(
284        "Downloading adapter {} from {}",
285        adapter_name,
286        &github_version.url,
287    );
288    delegate.output_to_console(format!("Downloading from {}...", github_version.url));
289
290    let mut response = delegate
291        .http_client()
292        .get(&github_version.url, Default::default(), true)
293        .await
294        .context("Error downloading release")?;
295    anyhow::ensure!(
296        response.status().is_success(),
297        "download failed with status {}",
298        response.status().to_string()
299    );
300
301    delegate.output_to_console("Download complete".to_owned());
302    match file_type {
303        DownloadedFileType::GzipTar => {
304            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
305            let archive = Archive::new(decompressed_bytes);
306            archive.unpack(&version_path).await?;
307        }
308        DownloadedFileType::Zip | DownloadedFileType::Vsix => {
309            let zip_path = version_path.with_extension("zip");
310            let mut file = File::create(&zip_path).await?;
311            futures::io::copy(response.body_mut(), &mut file).await?;
312            let file = File::open(&zip_path).await?;
313            extract_zip(&version_path, file)
314                .await
315                // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
316                .ok();
317
318            util::fs::remove_matching(&adapter_path, |entry| {
319                entry
320                    .file_name()
321                    .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
322            })
323            .await;
324        }
325    }
326
327    // remove older versions
328    util::fs::remove_matching(&adapter_path, |entry| {
329        entry.to_string_lossy() != version_path.to_string_lossy()
330    })
331    .await;
332
333    Ok(version_path)
334}
335
336pub async fn fetch_latest_adapter_version_from_github(
337    github_repo: GithubRepo,
338    delegate: &dyn DapDelegate,
339) -> Result<AdapterVersion> {
340    let release = latest_github_release(
341        &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
342        false,
343        false,
344        delegate.http_client(),
345    )
346    .await?;
347
348    Ok(AdapterVersion {
349        tag_name: release.tag_name,
350        url: release.zipball_url,
351    })
352}
353
354#[async_trait(?Send)]
355pub trait DebugAdapter: 'static + Send + Sync {
356    fn name(&self) -> DebugAdapterName;
357
358    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
359
360    async fn get_binary(
361        &self,
362        delegate: &Arc<dyn DapDelegate>,
363        config: &DebugTaskDefinition,
364        user_installed_path: Option<PathBuf>,
365        cx: &mut AsyncApp,
366    ) -> Result<DebugAdapterBinary>;
367
368    /// Returns the language name of an adapter if it only supports one language
369    fn adapter_language_name(&self) -> Option<LanguageName> {
370        None
371    }
372
373    /// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
374    /// This method should only return error when the kind cannot be determined for a given configuration;
375    /// 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.
376    fn request_kind(
377        &self,
378        config: &serde_json::Value,
379    ) -> Result<StartDebuggingRequestArgumentsRequest> {
380        match config.get("request") {
381            Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
382            Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
383            _ => Err(anyhow!(
384                "missing or invalid `request` field in config. Expected 'launch' or 'attach'"
385            )),
386        }
387    }
388
389    async fn dap_schema(&self) -> serde_json::Value;
390}
391
392#[cfg(any(test, feature = "test-support"))]
393pub struct FakeAdapter {}
394
395#[cfg(any(test, feature = "test-support"))]
396impl FakeAdapter {
397    pub const ADAPTER_NAME: &'static str = "fake-adapter";
398
399    pub fn new() -> Self {
400        Self {}
401    }
402}
403
404#[cfg(any(test, feature = "test-support"))]
405#[async_trait(?Send)]
406impl DebugAdapter for FakeAdapter {
407    fn name(&self) -> DebugAdapterName {
408        DebugAdapterName(Self::ADAPTER_NAME.into())
409    }
410
411    async fn dap_schema(&self) -> serde_json::Value {
412        serde_json::Value::Null
413    }
414
415    fn request_kind(
416        &self,
417        config: &serde_json::Value,
418    ) -> Result<StartDebuggingRequestArgumentsRequest> {
419        let request = config.as_object().unwrap()["request"].as_str().unwrap();
420
421        let request = match request {
422            "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
423            "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
424            _ => unreachable!("Wrong fake adapter input for request field"),
425        };
426
427        Ok(request)
428    }
429
430    fn adapter_language_name(&self) -> Option<LanguageName> {
431        None
432    }
433
434    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
435        let config = serde_json::to_value(zed_scenario.request).unwrap();
436
437        Ok(DebugScenario {
438            adapter: zed_scenario.adapter,
439            label: zed_scenario.label,
440            build: None,
441            config,
442            tcp_connection: None,
443        })
444    }
445
446    async fn get_binary(
447        &self,
448        _: &Arc<dyn DapDelegate>,
449        task_definition: &DebugTaskDefinition,
450        _: Option<PathBuf>,
451        _: &mut AsyncApp,
452    ) -> Result<DebugAdapterBinary> {
453        Ok(DebugAdapterBinary {
454            command: "command".into(),
455            arguments: vec![],
456            connection: None,
457            envs: HashMap::default(),
458            cwd: None,
459            request_args: StartDebuggingRequestArguments {
460                request: self.request_kind(&task_definition.config)?,
461                configuration: task_definition.config.clone(),
462            },
463        })
464    }
465}