diff --git a/Cargo.lock b/Cargo.lock index 33f84e94dfa3b..ea1a65b7bef17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3355,6 +3355,7 @@ dependencies = [ "log", "node_runtime", "parking_lot", + "paths", "schemars", "serde", "serde_json", @@ -3362,6 +3363,7 @@ dependencies = [ "smallvec", "smol", "task", + "util", ] [[package]] @@ -3389,6 +3391,7 @@ dependencies = [ "serde_json", "smol", "task", + "util", ] [[package]] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index bf5c574dc66c3..f3a955859869a 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -19,6 +19,7 @@ http_client.workspace = true log.workspace = true node_runtime.workspace = true parking_lot.workspace = true +paths.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true @@ -26,3 +27,4 @@ settings.workspace = true smallvec.workspace = true smol.workspace = true task.workspace = true +util.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 119dd113d781f..02e221088972f 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,11 +1,18 @@ use crate::transport::Transport; use ::fs::Fs; -use anyhow::Result; +use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; -use http_client::HttpClient; +use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; use serde_json::Value; -use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc}; +use smol::{self, fs::File, process}; +use std::{ + collections::HashMap, + ffi::OsString, + fmt::Debug, + path::{Path, PathBuf}, + sync::Arc, +}; use task::DebugAdapterConfig; pub trait DapDelegate { @@ -35,8 +42,86 @@ pub struct DebugAdapterBinary { pub envs: Option>, } +pub async fn download_adapter_from_github( + adapter_name: DebugAdapterName, + github_repo: GithubRepo, + delegate: &dyn DapDelegate, +) -> Result { + let adapter_path = paths::debug_adapters_dir().join(&adapter_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 _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?; + + 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); + } + } + + bail!("Install failed to download & counldn't preinstalled dap") +} + +pub struct GithubRepo { + pub repo_name: String, + pub repo_owner: String, +} + #[async_trait(?Send)] pub trait DebugAdapter: 'static + Send + Sync { + fn id(&self) -> String { + "".to_string() + } + fn name(&self) -> DebugAdapterName; fn transport(&self) -> Box; diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index f97e0435dc790..3e50c1eed82ff 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -25,3 +25,4 @@ serde.workspace = true serde_json.workspace = true smol.workspace = true task.workspace = true +util.workspace = true diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 1116cd1da89f6..39edd6dfae3dd 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -4,22 +4,18 @@ mod lldb; mod php; mod python; +use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; use custom::CustomDebugAdapter; +use dap::adapters::{ + self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, +}; use javascript::JsDebugAdapter; use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; - -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -use dap::adapters::{DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; -use http_client::github::latest_github_release; use serde_json::{json, Value}; -use smol::{ - fs::{self, File}, - process, -}; -use std::{fmt::Debug, process::Stdio}; +use std::fmt::Debug; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 4fafc9b85f7a5..dc4f4d02bd9f9 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -38,6 +38,11 @@ impl DebugAdapter for JsDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with("vscode-js-debug_") + }) + .await + .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; Ok(DebugAdapterBinary { command: node_runtime @@ -54,102 +59,29 @@ impl DebugAdapter for JsDebugAdapter { } async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - - if fs.is_dir(adapter_path.as_path()).await { - return Ok(()); - } - - if let Some(http_client) = delegate.http_client() { - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; - } - - let release = latest_github_release( - "microsoft/vscode-js-debug", - false, - false, - http_client.clone(), - ) - .await?; - - let asset_name = format!("{}-{}", self.name(), release.tag_name); - let zip_path = adapter_path.join(asset_name); - - if 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 _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; + let github_repo = GithubRepo { + repo_name: "vscode-js-debug".to_string(), + repo_owner: "microsoft".to_string(), + }; - let mut ls = process::Command::new("ls") - .current_dir(&adapter_path) - .stdout(Stdio::piped()) - .spawn()?; + let adapter_path = + adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; - let std = ls - .stdout - .take() - .ok_or(anyhow!("Failed to list directories"))? - .into_stdio() - .await?; - - let file_name = String::from_utf8( - process::Command::new("grep") - .arg("microsoft-vscode-js-debug") - .stdin(std) - .output() - .await? - .stdout, - )?; - - let file_name = file_name.trim_end(); - - process::Command::new("sh") - .current_dir(&adapter_path) - .arg("-c") - .arg(format!("mv {file_name}/* .")) - .output() - .await?; - - process::Command::new("rm") - .current_dir(&adapter_path) - .arg("-rf") - .arg(file_name) - .arg(zip_path) - .output() - .await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["compile"]) - .await - .ok(); + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "install", &[]) + .await + .ok(); - return Ok(()); - } - } + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "run", &["compile"]) + .await + .ok(); - bail!("Install or fetch not implemented for Javascript debug adapter (yet)"); + return Ok(()); } fn request_args(&self, config: &DebugAdapterConfig) -> Value { diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 4d8e5e17c1e4b..0911597e0490c 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -26,8 +26,8 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { - bail!("Install or fetch not implemented for lldb debug adapter (yet)") + async fn install_binary(&self, _delegate: &dyn DapDelegate) -> Result<()> { + bail!("Install binary is not support for install_binary (yet)") } async fn fetch_binary( @@ -35,7 +35,25 @@ impl DebugAdapter for LldbDebugAdapter { _: &dyn DapDelegate, _: &DebugAdapterConfig, ) -> Result { - bail!("Install or fetch not implemented for lldb debug adapter (yet)") + #[cfg(target_os = "macos")] + { + let output = std::process::Command::new("xcrun") + .args(&["-f", "lldb-dap"]) + .output()?; + let lldb_dap_path = String::from_utf8(output.stdout)?.trim().to_string(); + + Ok(DebugAdapterBinary { + command: lldb_dap_path, + arguments: None, + envs: None, + }) + } + #[cfg(not(target_os = "macos"))] + { + Err(anyhow::anyhow!( + "LLDB-DAP is only supported on macOS (Right now)" + )) + } } fn request_args(&self, config: &DebugAdapterConfig) -> Value { diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 9e5c647eab20b..f9cf3860018ca 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -38,6 +38,11 @@ impl DebugAdapter for PhpDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with("vscode-php-debug_") + }) + .await + .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; Ok(DebugAdapterBinary { command: node_runtime @@ -54,98 +59,29 @@ impl DebugAdapter for PhpDebugAdapter { } async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - - if fs.is_dir(adapter_path.as_path()).await { - return Ok(()); - } - - if let Some(http_client) = delegate.http_client() { - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; - } - - let release = - latest_github_release("xdebug/vscode-php-debug", false, false, http_client.clone()) - .await?; - - let asset_name = format!("{}-{}", self.name(), release.tag_name); - let zip_path = adapter_path.join(asset_name); - - if 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 _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; + let github_repo = GithubRepo { + repo_name: "vscode-php-debug".to_string(), + repo_owner: "xdebug".to_string(), + }; - let mut ls = process::Command::new("ls") - .current_dir(&adapter_path) - .stdout(Stdio::piped()) - .spawn()?; + let adapter_path = + adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; - let std = ls - .stdout - .take() - .ok_or(anyhow!("Failed to list directories"))? - .into_stdio() - .await?; - - let file_name = String::from_utf8( - process::Command::new("grep") - .arg("xdebug-vscode-php-debug") - .stdin(std) - .output() - .await? - .stdout, - )?; - - let file_name = file_name.trim_end(); - - process::Command::new("sh") - .current_dir(&adapter_path) - .arg("-c") - .arg(format!("mv {file_name}/* .")) - .output() - .await?; - - process::Command::new("rm") - .current_dir(&adapter_path) - .arg("-rf") - .arg(file_name) - .arg(zip_path) - .output() - .await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .is_ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["build"]) - .await - .is_ok(); + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "install", &[]) + .await + .is_ok(); - return Ok(()); - } - } + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "run", &["build"]) + .await + .is_ok(); - bail!("Install or fetch not implemented for PHP debug adapter (yet)"); + Ok(()) } fn request_args(&self, config: &DebugAdapterConfig) -> Value { diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index da96bb0dd0fa1..e339289b8d0a5 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -24,6 +24,15 @@ impl DebugAdapter for PythonDebugAdapter { Box::new(StdioTransport::new()) } + async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { + let github_repo = GithubRepo { + repo_name: "debugpy".into(), + repo_owner: "microsoft".into(), + }; + + adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + Ok(()) + } async fn fetch_binary( &self, _: &dyn DapDelegate, @@ -31,95 +40,19 @@ impl DebugAdapter for PythonDebugAdapter { ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name()); + let debugpy_dir = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with("debugpy_") + }) + .await + .ok_or_else(|| anyhow!("Debugpy directory not found"))?; + Ok(DebugAdapterBinary { command: "python3".to_string(), - arguments: Some(vec![adapter_path.join(Self::ADAPTER_PATH).into()]), + arguments: Some(vec![debugpy_dir.join(Self::ADAPTER_PATH).into()]), envs: None, }) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - - if fs.is_dir(adapter_path.as_path()).await { - return Ok(()); - } - - if let Some(http_client) = delegate.http_client() { - let debugpy_dir = paths::debug_adapters_dir().join("debugpy"); - - if !debugpy_dir.exists() { - fs.create_dir(&debugpy_dir.as_path()).await?; - } - - let release = - latest_github_release("microsoft/debugpy", false, false, http_client.clone()) - .await?; - let asset_name = format!("{}.zip", release.tag_name); - - let zip_path = debugpy_dir.join(asset_name); - - if 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 _unzip_status = process::Command::new("unzip") - .current_dir(&debugpy_dir) - .arg(&zip_path) - .output() - .await? - .status; - - let mut ls = process::Command::new("ls") - .current_dir(&debugpy_dir) - .stdout(Stdio::piped()) - .spawn()?; - - let std = ls - .stdout - .take() - .ok_or(anyhow!("Failed to list directories"))? - .into_stdio() - .await?; - - let file_name = String::from_utf8( - process::Command::new("grep") - .arg("microsoft-debugpy") - .stdin(std) - .output() - .await? - .stdout, - )?; - - let file_name = file_name.trim_end(); - process::Command::new("sh") - .current_dir(&debugpy_dir) - .arg("-c") - .arg(format!("mv {file_name}/* .")) - .output() - .await?; - - process::Command::new("rm") - .current_dir(&debugpy_dir) - .arg("-rf") - .arg(file_name) - .arg(zip_path) - .output() - .await?; - - return Ok(()); - } - } - - bail!("Install or fetch not implemented for Python debug adapter (yet)"); - } - fn request_args(&self, config: &DebugAdapterConfig) -> Value { json!({"program": config.program, "subProcess": true}) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 40a242a83d4ec..4966deaecc8a2 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -250,17 +250,22 @@ impl DapStore { .context("Creating debug adapter") .log_err()?, ); - let _ = adapter - .install_binary(&adapter_delegate) - .await - .context("Failed to install debug adapter binary") - .log_err()?; - let binary = adapter - .fetch_binary(&adapter_delegate, &config) - .await - .context("Failed to get debug adapter binary") - .log_err()?; + let mut binary = adapter.fetch_binary(&adapter_delegate, &config).await.ok(); + + if binary.is_none() { + let _ = adapter + .install_binary(&adapter_delegate) + .await + .context("Failed to install debug adapter binary") + .log_err()?; + + binary = adapter + .fetch_binary(&adapter_delegate, &config) + .await + .context("Failed to get debug adapter binary") + .log_err(); + } let mut request_args = json!({}); if let Some(config_args) = config.initialize_args.clone() { @@ -277,7 +282,7 @@ impl DapStore { client .start( - &binary, + &binary?, move |message, cx| { dap_store .update(cx, |_, cx| { diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index f235753e8b2af..93340729df2b4 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::ResultExt; use async_fs as fs; @@ -26,3 +26,24 @@ where } } } + +pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option +where + F: Fn(&str) -> bool, +{ + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + + if let Some(file_name) = entry_path.file_name() { + if predicate(file_name.to_str().unwrap_or("")) { + return Some(entry_path); + } + } + } + } + } + + None +}