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