use ::fs::Fs;use anyhow::{Context as _, Ok, Result, anyhow};use async_compression::futures::bufread::GzipDecoder;use async_tar::Archive;use async_trait::async_trait;use dap_types::StartDebuggingRequestArguments;use futures::io::BufReader;use gpui::{AsyncApp, SharedString};pub use http_client::{HttpClient, github::latest_github_release};use language::LanguageToolchainStore;use node_runtime::NodeRuntime;use serde::{Deserialize, Serialize};use settings::WorktreeId;use smol::{self, fs::File, lock::Mutex};use std::{ borrow::Borrow, collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, fmt::Debug, net::Ipv4Addr, ops::Deref, path::PathBuf, sync::Arc,};use task::DebugTaskDefinition;use util::ResultExt; #[derive(Clone, Debug, PartialEq, Eq)]pub enum DapStatus { None, CheckingForUpdate, Downloading, Failed { error: String },} #[async_trait(?Send)]pub trait DapDelegate { fn worktree_id(&self) -> WorktreeId; fn http_client(&self) -> Arc<dyn HttpClient>; fn node_runtime(&self) -> NodeRuntime; fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>; fn fs(&self) -> Arc<dyn Fs>; fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>; fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus); fn which(&self, command: &OsStr) -> Option<PathBuf>; async fn shell_env(&self) -> collections::HashMap<String, String>;} #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]pub struct DebugAdapterName(pub SharedString); impl Deref for DebugAdapterName { type Target = str; fn deref(&self) -> &Self::Target { &self.0 }} impl AsRef<str> for DebugAdapterName { fn as_ref(&self) -> &str { &self.0 }} impl Borrow<str> for DebugAdapterName { fn borrow(&self) -> &str { &self.0 }} impl std::fmt::Display for DebugAdapterName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) }} impl From<DebugAdapterName> for SharedString { fn from(name: DebugAdapterName) -> Self { name.0 }} impl<'a> From<&'a str> for DebugAdapterName { fn from(str: &'a str) -> DebugAdapterName { DebugAdapterName(str.to_string().into()) }} #[derive(Debug, Clone)]pub struct TcpArguments { pub host: Ipv4Addr, pub port: u16, pub timeout: Option<u64>,}#[derive(Debug, Clone)]pub struct DebugAdapterBinary { pub adapter_name: DebugAdapterName, pub command: String, pub arguments: Option<Vec<OsString>>, pub envs: Option<HashMap<String, String>>, pub cwd: Option<PathBuf>, pub connection: Option<TcpArguments>, pub request_args: StartDebuggingRequestArguments,} #[derive(Debug)]pub struct AdapterVersion { pub tag_name: String, pub url: String,} pub enum DownloadedFileType { Vsix, GzipTar, Zip,} pub struct GithubRepo { pub repo_name: String, pub repo_owner: String,} pub async fn download_adapter_from_github( adapter_name: DebugAdapterName, github_version: AdapterVersion, file_type: DownloadedFileType, delegate: &dyn DapDelegate,) -> Result<PathBuf> { let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref()); let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); let fs = delegate.fs(); if version_path.exists() { return Ok(version_path); } if !adapter_path.exists() { fs.create_dir(&adapter_path.as_path()) .await .context("Failed creating adapter path")?; } log::debug!( "Downloading adapter {} from {}", adapter_name, &github_version.url, ); let mut response = delegate .http_client() .get(&github_version.url, Default::default(), true) .await .context("Error downloading release")?; if !response.status().is_success() { Err(anyhow!( "download failed with status {}", response.status().to_string() ))?; } match file_type { DownloadedFileType::GzipTar => { let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); let archive = Archive::new(decompressed_bytes); archive.unpack(&version_path).await?; } DownloadedFileType::Zip | DownloadedFileType::Vsix => { let zip_path = version_path.with_extension("zip"); let mut file = File::create(&zip_path).await?; futures::io::copy(response.body_mut(), &mut file).await?; // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence` util::command::new_smol_command("unzip") .arg(&zip_path) .arg("-d") .arg(&version_path) .output() .await?; util::fs::remove_matching(&adapter_path, |entry| { entry .file_name() .is_some_and(|file| file.to_string_lossy().ends_with(".zip")) }) .await; } } // remove older versions util::fs::remove_matching(&adapter_path, |entry| { entry.to_string_lossy() != version_path.to_string_lossy() }) .await; Ok(version_path)} pub async fn fetch_latest_adapter_version_from_github( github_repo: GithubRepo, delegate: &dyn DapDelegate,) -> Result<AdapterVersion> { let release = latest_github_release( &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), false, false, delegate.http_client(), ) .await?; Ok(AdapterVersion { tag_name: release.tag_name, url: release.zipball_url, })} #[async_trait(?Send)]pub trait DebugAdapter: 'static + Send + Sync { fn name(&self) -> DebugAdapterName; async fn get_binary( &self, delegate: &dyn DapDelegate, config: &DebugTaskDefinition, user_installed_path: Option<PathBuf>, cx: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { if delegate .updated_adapters() .lock() .await .contains(&self.name()) { log::info!("Using cached debug adapter binary {}", self.name()); if let Some(binary) = self .get_installed_binary(delegate, &config, user_installed_path.clone(), cx) .await .log_err() { return Ok(binary); } log::info!( "Cached binary {} is corrupt falling back to install", self.name() ); } log::info!("Getting latest version of debug adapter {}", self.name()); delegate.update_status(self.name(), DapStatus::CheckingForUpdate); if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { log::info!( "Installiing latest version of debug adapter {}", self.name() ); delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; delegate .updated_adapters() .lock_arc() .await .insert(self.name()); } self.get_installed_binary(delegate, &config, user_installed_path, cx) .await } async fn fetch_latest_adapter_version( &self, delegate: &dyn DapDelegate, ) -> Result<AdapterVersion>; /// Installs the binary for the debug adapter. /// This method is called when the adapter binary is not found or needs to be updated. /// It should download and install the necessary files for the debug adapter to function. async fn install_binary( &self, version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()>; async fn get_installed_binary( &self, delegate: &dyn DapDelegate, config: &DebugTaskDefinition, user_installed_path: Option<PathBuf>, cx: &mut AsyncApp, ) -> Result<DebugAdapterBinary>;}#[cfg(any(test, feature = "test-support"))]pub struct FakeAdapter {} #[cfg(any(test, feature = "test-support"))]impl FakeAdapter { pub const ADAPTER_NAME: &'static str = "fake-adapter"; pub fn new() -> Self { Self {} } fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments { use serde_json::json; use task::DebugRequestType; let value = json!({ "request": match config.request { DebugRequestType::Launch(_) => "launch", DebugRequestType::Attach(_) => "attach", }, "process_id": if let DebugRequestType::Attach(attach_config) = &config.request { attach_config.process_id } else { None }, }); let request = match config.request { DebugRequestType::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch, DebugRequestType::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach, }; StartDebuggingRequestArguments { configuration: value, request, } }} #[cfg(any(test, feature = "test-support"))]#[async_trait(?Send)]impl DebugAdapter for FakeAdapter { fn name(&self) -> DebugAdapterName { DebugAdapterName(Self::ADAPTER_NAME.into()) } async fn get_binary( &self, _: &dyn DapDelegate, config: &DebugTaskDefinition, _: Option<PathBuf>, _: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { Ok(DebugAdapterBinary { adapter_name: Self::ADAPTER_NAME.into(), command: "command".into(), arguments: None, connection: None, envs: None, cwd: None, request_args: self.request_args(config), }) } async fn fetch_latest_adapter_version( &self, _delegate: &dyn DapDelegate, ) -> Result<AdapterVersion> { unimplemented!("fetch latest adapter version"); } async fn install_binary( &self, _version: AdapterVersion, _delegate: &dyn DapDelegate, ) -> Result<()> { unimplemented!("install binary"); } async fn get_installed_binary( &self, _: &dyn DapDelegate, _: &DebugTaskDefinition, _: Option<PathBuf>, _: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { unimplemented!("get installed binary"); }}
use ::fs::Fs;
use anyhow::{Context as _, Ok, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use dap_types::StartDebuggingRequestArguments;
use futures::io::BufReader;
use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::LanguageToolchainStore;
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::{self, fs::File, lock::Mutex};
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fmt::Debug,
net::Ipv4Addr,
ops::Deref,
path::PathBuf,
sync::Arc,
};
use task::DebugTaskDefinition;
use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
None,
CheckingForUpdate,
Downloading,
Failed { error: String },
}
#[async_trait(?Send)]
pub trait DapDelegate {
fn worktree_id(&self) -> WorktreeId;
fn http_client(&self) -> Arc<dyn HttpClient>;
fn node_runtime(&self) -> NodeRuntime;
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
fn fs(&self) -> Arc<dyn Fs>;
fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct DebugAdapterName(pub SharedString);
impl Deref for DebugAdapterName {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for DebugAdapterName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for DebugAdapterName {
fn borrow(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for DebugAdapterName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl From<DebugAdapterName> for SharedString {
fn from(name: DebugAdapterName) -> Self {
name.0
}
}
impl<'a> From<&'a str> for DebugAdapterName {
fn from(str: &'a str) -> DebugAdapterName {
DebugAdapterName(str.to_string().into())
}
}
#[derive(Debug, Clone)]
pub struct TcpArguments {
pub host: Ipv4Addr,
pub port: u16,
pub timeout: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct DebugAdapterBinary {
pub adapter_name: DebugAdapterName,
pub command: String,
pub arguments: Option<Vec<OsString>>,
pub envs: Option<HashMap<String, String>>,
pub cwd: Option<PathBuf>,
pub connection: Option<TcpArguments>,
pub request_args: StartDebuggingRequestArguments,
}
#[derive(Debug)]
pub struct AdapterVersion {
pub tag_name: String,
pub url: String,
}
pub enum DownloadedFileType {
Vsix,
GzipTar,
Zip,
}
pub struct GithubRepo {
pub repo_name: String,
pub repo_owner: String,
}
pub async fn download_adapter_from_github(
adapter_name: DebugAdapterName,
github_version: AdapterVersion,
file_type: DownloadedFileType,
delegate: &dyn DapDelegate,
) -> Result<PathBuf> {
let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
let fs = delegate.fs();
if version_path.exists() {
return Ok(version_path);
}
if !adapter_path.exists() {
fs.create_dir(&adapter_path.as_path())
.await
.context("Failed creating adapter path")?;
}
log::debug!(
"Downloading adapter {} from {}",
adapter_name,
&github_version.url,
);
let mut response = delegate
.http_client()
.get(&github_version.url, Default::default(), true)
.await
.context("Error downloading release")?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
match file_type {
DownloadedFileType::GzipTar => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&version_path).await?;
}
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
let zip_path = version_path.with_extension("zip");
let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
util::command::new_smol_command("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_path)
.output()
.await?;
util::fs::remove_matching(&adapter_path, |entry| {
entry
.file_name()
.is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
})
.await;
}
}
// remove older versions
util::fs::remove_matching(&adapter_path, |entry| {
entry.to_string_lossy() != version_path.to_string_lossy()
})
.await;
Ok(version_path)
}
pub async fn fetch_latest_adapter_version_from_github(
github_repo: GithubRepo,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
false,
false,
delegate.http_client(),
)
.await?;
Ok(AdapterVersion {
tag_name: release.tag_name,
url: release.zipball_url,
})
}
#[async_trait(?Send)]
pub trait DebugAdapter: 'static + Send + Sync {
fn name(&self) -> DebugAdapterName;
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if delegate
.updated_adapters()
.lock()
.await
.contains(&self.name())
{
log::info!("Using cached debug adapter binary {}", self.name());
if let Some(binary) = self
.get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
.await
.log_err()
{
return Ok(binary);
}
log::info!(
"Cached binary {} is corrupt falling back to install",
self.name()
);
}
log::info!("Getting latest version of debug adapter {}", self.name());
delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
log::info!(
"Installiing latest version of debug adapter {}",
self.name()
);
delegate.update_status(self.name(), DapStatus::Downloading);
self.install_binary(version, delegate).await?;
delegate
.updated_adapters()
.lock_arc()
.await
.insert(self.name());
}
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion>;
/// Installs the binary for the debug adapter.
/// This method is called when the adapter binary is not found or needs to be updated.
/// It should download and install the necessary files for the debug adapter to function.
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()>;
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeAdapter {}
#[cfg(any(test, feature = "test-support"))]
impl FakeAdapter {
pub const ADAPTER_NAME: &'static str = "fake-adapter";
pub fn new() -> Self {
Self {}
}
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
use serde_json::json;
use task::DebugRequestType;
let value = json!({
"request": match config.request {
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
},
});
let request = match config.request {
DebugRequestType::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequestType::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments {
configuration: value,
request,
}
}
}
#[cfg(any(test, feature = "test-support"))]
#[async_trait(?Send)]
impl DebugAdapter for FakeAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn get_binary(
&self,
_: &dyn DapDelegate,
config: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
Ok(DebugAdapterBinary {
adapter_name: Self::ADAPTER_NAME.into(),
command: "command".into(),
arguments: None,
connection: None,
envs: None,
cwd: None,
request_args: self.request_args(config),
})
}
async fn fetch_latest_adapter_version(
&self,
_delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
unimplemented!("fetch latest adapter version");
}
async fn install_binary(
&self,
_version: AdapterVersion,
_delegate: &dyn DapDelegate,
) -> Result<()> {
unimplemented!("install binary");
}
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
unimplemented!("get installed binary");
}
}