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 match file_type {
302 DownloadedFileType::GzipTar => {
303 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
304 let archive = Archive::new(decompressed_bytes);
305 archive.unpack(&version_path).await?;
306 }
307 DownloadedFileType::Zip | DownloadedFileType::Vsix => {
308 let zip_path = version_path.with_extension("zip");
309 let mut file = File::create(&zip_path).await?;
310 futures::io::copy(response.body_mut(), &mut file).await?;
311 let file = File::open(&zip_path).await?;
312 extract_zip(&version_path, file)
313 .await
314 // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
315 .ok();
316
317 util::fs::remove_matching(&adapter_path, |entry| {
318 entry
319 .file_name()
320 .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
321 })
322 .await;
323 }
324 }
325
326 // remove older versions
327 util::fs::remove_matching(&adapter_path, |entry| {
328 entry.to_string_lossy() != version_path.to_string_lossy()
329 })
330 .await;
331
332 Ok(version_path)
333}
334
335pub async fn fetch_latest_adapter_version_from_github(
336 github_repo: GithubRepo,
337 delegate: &dyn DapDelegate,
338) -> Result<AdapterVersion> {
339 let release = latest_github_release(
340 &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
341 false,
342 false,
343 delegate.http_client(),
344 )
345 .await?;
346
347 Ok(AdapterVersion {
348 tag_name: release.tag_name,
349 url: release.zipball_url,
350 })
351}
352
353#[async_trait(?Send)]
354pub trait DebugAdapter: 'static + Send + Sync {
355 fn name(&self) -> DebugAdapterName;
356
357 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
358
359 async fn get_binary(
360 &self,
361 delegate: &Arc<dyn DapDelegate>,
362 config: &DebugTaskDefinition,
363 user_installed_path: Option<PathBuf>,
364 cx: &mut AsyncApp,
365 ) -> Result<DebugAdapterBinary>;
366
367 /// Returns the language name of an adapter if it only supports one language
368 fn adapter_language_name(&self) -> Option<LanguageName> {
369 None
370 }
371
372 fn validate_config(
373 &self,
374 config: &serde_json::Value,
375 ) -> Result<StartDebuggingRequestArgumentsRequest> {
376 let map = config.as_object().context("Config isn't an object")?;
377
378 let request_variant = map
379 .get("request")
380 .and_then(|val| val.as_str())
381 .context("request argument is not found or invalid")?;
382
383 match request_variant {
384 "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
385 "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
386 _ => Err(anyhow!("request must be either 'launch' or 'attach'")),
387 }
388 }
389
390 async fn dap_schema(&self) -> serde_json::Value;
391}
392
393#[cfg(any(test, feature = "test-support"))]
394pub struct FakeAdapter {}
395
396#[cfg(any(test, feature = "test-support"))]
397impl FakeAdapter {
398 pub const ADAPTER_NAME: &'static str = "fake-adapter";
399
400 pub fn new() -> Self {
401 Self {}
402 }
403}
404
405#[cfg(any(test, feature = "test-support"))]
406#[async_trait(?Send)]
407impl DebugAdapter for FakeAdapter {
408 fn name(&self) -> DebugAdapterName {
409 DebugAdapterName(Self::ADAPTER_NAME.into())
410 }
411
412 async fn dap_schema(&self) -> serde_json::Value {
413 serde_json::Value::Null
414 }
415
416 fn validate_config(
417 &self,
418 config: &serde_json::Value,
419 ) -> Result<StartDebuggingRequestArgumentsRequest> {
420 let request = config.as_object().unwrap()["request"].as_str().unwrap();
421
422 let request = match request {
423 "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
424 "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
425 _ => unreachable!("Wrong fake adapter input for request field"),
426 };
427
428 Ok(request)
429 }
430
431 fn adapter_language_name(&self) -> Option<LanguageName> {
432 None
433 }
434
435 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
436 let config = serde_json::to_value(zed_scenario.request).unwrap();
437
438 Ok(DebugScenario {
439 adapter: zed_scenario.adapter,
440 label: zed_scenario.label,
441 build: None,
442 config,
443 tcp_connection: None,
444 })
445 }
446
447 async fn get_binary(
448 &self,
449 _: &Arc<dyn DapDelegate>,
450 task_definition: &DebugTaskDefinition,
451 _: Option<PathBuf>,
452 _: &mut AsyncApp,
453 ) -> Result<DebugAdapterBinary> {
454 Ok(DebugAdapterBinary {
455 command: "command".into(),
456 arguments: vec![],
457 connection: None,
458 envs: HashMap::default(),
459 cwd: None,
460 request_args: StartDebuggingRequestArguments {
461 request: self.validate_config(&task_definition.config)?,
462 configuration: task_definition.config.clone(),
463 },
464 })
465 }
466}