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}