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, BufReader::new(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["request"]
379 .as_str()
380 .ok_or_else(|| anyhow!("request is not valid"))?;
381
382 match request_variant {
383 "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
384 "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
385 _ => Err(anyhow!("request must be either 'launch' or 'attach'")),
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 fn request_args(
404 &self,
405 task_definition: &DebugTaskDefinition,
406 ) -> StartDebuggingRequestArguments {
407 use serde_json::json;
408
409 let obj = task_definition.config.as_object().unwrap();
410
411 let request_variant = obj["request"].as_str().unwrap();
412
413 let value = json!({
414 "request": request_variant,
415 "process_id": obj.get("process_id"),
416 "raw_request": serde_json::to_value(task_definition).unwrap()
417 });
418
419 StartDebuggingRequestArguments {
420 configuration: value,
421 request: match request_variant {
422 "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
423 "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
424 _ => unreachable!("Wrong fake adapter input for request field"),
425 },
426 }
427 }
428}
429
430#[cfg(any(test, feature = "test-support"))]
431#[async_trait(?Send)]
432impl DebugAdapter for FakeAdapter {
433 fn name(&self) -> DebugAdapterName {
434 DebugAdapterName(Self::ADAPTER_NAME.into())
435 }
436
437 async fn dap_schema(&self) -> serde_json::Value {
438 serde_json::Value::Null
439 }
440
441 fn validate_config(
442 &self,
443 config: &serde_json::Value,
444 ) -> Result<StartDebuggingRequestArgumentsRequest> {
445 let request = config.as_object().unwrap()["request"].as_str().unwrap();
446
447 let request = match request {
448 "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
449 "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
450 _ => unreachable!("Wrong fake adapter input for request field"),
451 };
452
453 Ok(request)
454 }
455
456 fn adapter_language_name(&self) -> Option<LanguageName> {
457 None
458 }
459
460 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
461 let config = serde_json::to_value(zed_scenario.request).unwrap();
462
463 Ok(DebugScenario {
464 adapter: zed_scenario.adapter,
465 label: zed_scenario.label,
466 build: None,
467 config,
468 tcp_connection: None,
469 })
470 }
471
472 async fn get_binary(
473 &self,
474 _: &Arc<dyn DapDelegate>,
475 config: &DebugTaskDefinition,
476 _: Option<PathBuf>,
477 _: &mut AsyncApp,
478 ) -> Result<DebugAdapterBinary> {
479 Ok(DebugAdapterBinary {
480 command: "command".into(),
481 arguments: vec![],
482 connection: None,
483 envs: HashMap::default(),
484 cwd: None,
485 request_args: self.request_args(&config),
486 })
487 }
488}