Skip to content

Commit

Permalink
Enable Debug Adapter Updates (#53)
Browse files Browse the repository at this point in the history
* Get debug adapter to update when new versions are avaliable

* Debug adapter download only happens if there's a new version avaliable

* Use DebugAdapter.name() instead of string literals

* Fix bug where some daps wouldn't update/install correctly

* Clean up download adapter from github function

* Add debug adapter caching

* Add basic notification event to dap_store
  • Loading branch information
Anthony-Eid authored Oct 23, 2024
1 parent e2d449a commit 290c76d
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 118 deletions.
241 changes: 177 additions & 64 deletions crates/dap/src/adapters.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use crate::transport::Transport;
use ::fs::Fs;
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use http_client::{github::latest_github_release, HttpClient};
use node_runtime::NodeRuntime;
use serde_json::Value;
use smol::{self, fs::File, process};
use smol::{self, fs::File, lock::Mutex, process};
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
ffi::OsString,
fmt::Debug,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
};
Expand All @@ -19,10 +20,26 @@ pub trait DapDelegate {
fn http_client(&self) -> Option<Arc<dyn HttpClient>>;
fn node_runtime(&self) -> Option<NodeRuntime>;
fn fs(&self) -> Arc<dyn Fs>;
fn cached_binaries(&self) -> Arc<Mutex<HashMap<DebugAdapterName, DebugAdapterBinary>>>;
}

#[derive(PartialEq, Eq, Hash, Debug)]
pub struct DebugAdapterName(pub Arc<str>);

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 AsRef<Path> for DebugAdapterName {
fn as_ref(&self) -> &Path {
Path::new(&*self.0)
Expand All @@ -40,94 +57,190 @@ pub struct DebugAdapterBinary {
pub command: String,
pub arguments: Option<Vec<OsString>>,
pub envs: Option<HashMap<String, String>>,
pub version: String,
}

pub struct AdapterVersion {
pub tag_name: String,
pub url: String,
}

pub struct GithubRepo {
pub repo_name: String,
pub repo_owner: String,
}

pub async fn download_adapter_from_github(
adapter_name: DebugAdapterName,
github_repo: GithubRepo,
github_version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<PathBuf> {
let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
let version_dir = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
let fs = delegate.fs();

if let Some(http_client) = delegate.http_client() {
if !adapter_path.exists() {
fs.create_dir(&adapter_path.as_path()).await?;
}

let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name);
let release =
latest_github_release(&repo_name_with_owner, false, false, http_client.clone()).await?;

let asset_name = format!("{}_{}.zip", &adapter_name, release.tag_name);
let zip_path = adapter_path.join(&asset_name);

if smol::fs::metadata(&zip_path).await.is_err() {
let mut response = http_client
.get(&release.zipball_url, Default::default(), true)
.await
.context("Error downloading release")?;

let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;
let http_client = delegate
.http_client()
.ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?;

let _unzip_status = process::Command::new("unzip")
.current_dir(&adapter_path)
.arg(&zip_path)
.output()
.await?
.status;

fs.remove_file(&zip_path.as_path(), Default::default())
.await?;
if !adapter_path.exists() {
fs.create_dir(&adapter_path.as_path()).await?;
}

let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| {
file_name.contains(&adapter_name.to_string())
})
.await
.ok_or_else(|| anyhow!("Unzipped directory not found"));

let file_name = file_name?;
let downloaded_path = adapter_path
.join(format!("{}_{}", adapter_name, release.tag_name))
.to_owned();

fs.rename(
file_name.as_path(),
downloaded_path.as_path(),
Default::default(),
)
.await?;

// if !unzip_status.success() {
// dbg!(unzip_status);
// Err(anyhow!("failed to unzip downloaded dap archive"))?;
// }

return Ok(downloaded_path);
}
if version_dir.exists() {
return Ok(version_dir);
}

bail!("Install failed to download & counldn't preinstalled dap")
let asset_name = format!("{}_{}.zip", &adapter_name, github_version.tag_name);
let zip_path = adapter_path.join(&asset_name);
fs.remove_file(
zip_path.as_path(),
fs::RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;

let mut response = http_client
.get(&github_version.url, Default::default(), true)
.await
.context("Error downloading release")?;

let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;

let old_files: HashSet<_> = util::fs::collect_matching(&adapter_path.as_path(), |file_path| {
file_path != zip_path.as_path()
})
.await
.into_iter()
.filter_map(|file_path| {
file_path
.file_name()
.and_then(|f| f.to_str())
.map(|f| f.to_string())
})
.collect();

let _unzip_status = process::Command::new("unzip")
.current_dir(&adapter_path)
.arg(&zip_path)
.output()
.await?
.status;

let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| {
!file_name.ends_with(".zip") && !old_files.contains(file_name)
})
.await
.ok_or_else(|| anyhow!("Unzipped directory not found"));

let file_name = file_name?;
let downloaded_path = adapter_path
.join(format!("{}_{}", adapter_name, github_version.tag_name))
.to_owned();

fs.rename(
file_name.as_path(),
downloaded_path.as_path(),
Default::default(),
)
.await?;

util::fs::remove_matching(&adapter_path, |entry| entry != version_dir).await;

// if !unzip_status.success() {
// dbg!(unzip_status);
// Err(anyhow!("failed to unzip downloaded dap archive"))?;
// }

Ok(downloaded_path)
}

pub struct GithubRepo {
pub repo_name: String,
pub repo_owner: String,
pub async fn fetch_latest_adapter_version_from_github(
github_repo: GithubRepo,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let http_client = delegate
.http_client()
.ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?;
let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name);
let release = latest_github_release(&repo_name_with_owner, false, false, 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: &DebugAdapterConfig,
) -> Result<DebugAdapterBinary> {
if let Some(binary) = delegate.cached_binaries().lock().await.get(&self.name()) {
log::info!("Using cached debug adapter binary {}", self.name());
return Ok(binary.clone());
}

log::info!("Getting latest version of debug adapter {}", self.name());
let version = self.fetch_latest_adapter_version(delegate).await.ok();

let mut binary = self.get_installed_binary(delegate, config).await;

if let Some(version) = version {
if binary
.as_ref()
.is_ok_and(|binary| binary.version == version.tag_name)
{
let binary = binary?;

delegate
.cached_binaries()
.lock_arc()
.await
.insert(self.name(), binary.clone());

return Ok(binary);
}

self.install_binary(version, delegate).await?;
binary = self.get_installed_binary(delegate, config).await;
}

let binary = binary?;

delegate
.cached_binaries()
.lock_arc()
.await
.insert(self.name(), binary.clone());

Ok(binary)
}

fn transport(&self) -> Box<dyn Transport>;

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, delegate: &dyn DapDelegate) -> Result<()>;
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()>;

async fn fetch_binary(
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
config: &DebugAdapterConfig,
Expand Down
9 changes: 7 additions & 2 deletions crates/dap_adapters/src/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ impl DebugAdapter for CustomDebugAdapter {
}
}

async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> {
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
bail!("Custom debug adapters don't have latest versions")
}

async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> {
Ok(())
}

async fn fetch_binary(
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugAdapterConfig,
Expand All @@ -49,6 +53,7 @@ impl DebugAdapter for CustomDebugAdapter {
.clone()
.map(|args| args.iter().map(OsString::from).collect()),
envs: self.custom_args.envs.clone(),
version: "Custom daps".to_string(),
})
}

Expand Down
3 changes: 2 additions & 1 deletion crates/dap_adapters/src/dap_adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use custom::CustomDebugAdapter;
use dap::adapters::{
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo,
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
};
use javascript::JsDebugAdapter;
use lldb::LldbDebugAdapter;
Expand Down
Loading

0 comments on commit 290c76d

Please sign in to comment.