From 290c76daefbaea45e346af029a891ba6d9130a2e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:17:12 -0400 Subject: [PATCH] Enable Debug Adapter Updates (#53) * 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 --- crates/dap/src/adapters.rs | 241 +++++++++++++++++------- crates/dap_adapters/src/custom.rs | 9 +- crates/dap_adapters/src/dap_adapters.rs | 3 +- crates/dap_adapters/src/javascript.rs | 41 +++- crates/dap_adapters/src/lldb.rs | 12 +- crates/dap_adapters/src/php.rs | 41 +++- crates/dap_adapters/src/python.rs | 31 ++- crates/project/src/dap_store.rs | 38 ++-- crates/project/src/project.rs | 7 +- crates/util/src/fs.rs | 29 ++- 10 files changed, 334 insertions(+), 118 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 1c099baae6affa..8b0d2154be95dc 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -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, }; @@ -19,10 +20,26 @@ pub trait DapDelegate { fn http_client(&self) -> Option>; fn node_runtime(&self) -> Option; fn fs(&self) -> Arc; + fn cached_binaries(&self) -> Arc>>; } +#[derive(PartialEq, Eq, Hash, Debug)] pub struct DebugAdapterName(pub Arc); +impl Deref for DebugAdapterName { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for DebugAdapterName { + fn as_ref(&self) -> &str { + &self.0 + } +} + impl AsRef for DebugAdapterName { fn as_ref(&self) -> &Path { Path::new(&*self.0) @@ -40,94 +57,190 @@ pub struct DebugAdapterBinary { pub command: String, pub arguments: Option>, pub envs: Option>, + 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 { 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 { + 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 { + 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; + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result; + /// 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, diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 2ad9cd97500743..6da45ea1351660 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -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 { + 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, @@ -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(), }) } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 39edd6dfae3dd9..e099f92430fec9 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -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; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index dc4f4d02bd9f9f..2c59a9ae2cbd59 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -28,7 +28,19 @@ impl DebugAdapter for JsDebugAdapter { })) } - async fn fetch_binary( + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result { + let github_repo = GithubRepo { + repo_name: Self::ADAPTER_NAME.into(), + repo_owner: "microsoft".to_string(), + }; + + adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + } + + async fn get_installed_binary( &self, delegate: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -38,11 +50,20 @@ impl DebugAdapter for JsDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", 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_") + file_name.starts_with(&file_name_prefix) }) .await - .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; + .ok_or_else(|| anyhow!("Couldn't find Javascript dap directory"))?; + + let version = adapter_path + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Javascript debug adapter has invalid file name"))? + .to_string(); Ok(DebugAdapterBinary { command: node_runtime @@ -55,17 +76,17 @@ impl DebugAdapter for JsDebugAdapter { "8133".into(), ]), envs: None, + version, }) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let github_repo = GithubRepo { - repo_name: "vscode-js-debug".to_string(), - repo_owner: "microsoft".to_string(), - }; - + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { let adapter_path = - adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + adapters::download_adapter_from_github(self.name(), version, delegate).await?; let _ = delegate .node_runtime() diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 0911597e0490ce..bccb68d6e0cbe9 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -26,11 +26,19 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary(&self, _delegate: &dyn DapDelegate) -> Result<()> { + async fn install_binary( + &self, + _version: AdapterVersion, + _delegate: &dyn DapDelegate, + ) -> Result<()> { bail!("Install binary is not support for install_binary (yet)") } - async fn fetch_binary( + async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { + bail!("Fetch latest adapter version not implemented for lldb (yet)") + } + + async fn get_installed_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index f9cf3860018ca0..a5aa647822a4d4 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -28,7 +28,19 @@ impl DebugAdapter for PhpDebugAdapter { })) } - async fn fetch_binary( + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result { + let github_repo = GithubRepo { + repo_name: Self::ADAPTER_NAME.into(), + repo_owner: "xdebug".to_string(), + }; + + adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + } + + async fn get_installed_binary( &self, delegate: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -38,11 +50,20 @@ impl DebugAdapter for PhpDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", 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_") + file_name.starts_with(&file_name_prefix) }) .await - .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; + .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; + + let version = adapter_path + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Php debug adapter has invalid file name"))? + .to_string(); Ok(DebugAdapterBinary { command: node_runtime @@ -55,17 +76,17 @@ impl DebugAdapter for PhpDebugAdapter { "--server=8132".into(), ]), envs: None, + version, }) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let github_repo = GithubRepo { - repo_name: "vscode-php-debug".to_string(), - repo_owner: "xdebug".to_string(), - }; - + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { let adapter_path = - adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + adapters::download_adapter_from_github(self.name(), version, delegate).await?; let _ = delegate .node_runtime() diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index e339289b8d0a59..136b95c0972776 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -24,32 +24,53 @@ impl DebugAdapter for PythonDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result { let github_repo = GithubRepo { - repo_name: "debugpy".into(), + repo_name: Self::ADAPTER_NAME.into(), repo_owner: "microsoft".into(), }; - adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + } + + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { + adapters::download_adapter_from_github(self.name(), version, delegate).await?; Ok(()) } - async fn fetch_binary( + + async fn get_installed_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", self.name()); let debugpy_dir = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with("debugpy_") + file_name.starts_with(&file_name_prefix) }) .await .ok_or_else(|| anyhow!("Debugpy directory not found"))?; + let version = debugpy_dir + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Python debug adapter has invalid file name"))? + .to_string(); + Ok(DebugAdapterBinary { command: "python3".to_string(), arguments: Some(vec![debugpy_dir.join(Self::ADAPTER_PATH).into()]), envs: None, + version, }) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 4966deaecc8a2a..159d69b7392146 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,6 +1,7 @@ use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; -use collections::{HashMap, HashSet}; +use collections::HashSet; +use dap::adapters::{DebugAdapterBinary, DebugAdapterName}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ @@ -28,8 +29,9 @@ use language::{Buffer, BufferSnapshot}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; use settings::WorktreeId; +use smol::lock::Mutex; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::{ @@ -48,6 +50,7 @@ pub enum DapStoreEvent { client_id: DebugAdapterClientId, message: Message, }, + Notification(String), } pub enum DebugAdapterClientState { @@ -64,6 +67,7 @@ pub struct DebugPosition { pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, + cached_binaries: Arc>>, breakpoints: BTreeMap>, capabilities: HashMap, active_debug_line: Option<(ProjectPath, DebugPosition)>, @@ -86,6 +90,7 @@ impl DapStore { Self { active_debug_line: None, clients: Default::default(), + cached_binaries: Default::default(), breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), @@ -242,6 +247,7 @@ impl DapStore { self.http_client.clone(), self.node_runtime.clone(), self.fs.clone(), + self.cached_binaries.clone(), ); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); @@ -251,21 +257,10 @@ impl DapStore { .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 binary = adapter + .get_binary(&adapter_delegate, &config) + .await + .log_err()?; let mut request_args = json!({}); if let Some(config_args) = config.initialize_args.clone() { @@ -282,7 +277,7 @@ impl DapStore { client .start( - &binary?, + &binary, move |message, cx| { dap_store .update(cx, |_, cx| { @@ -1238,6 +1233,7 @@ pub struct DapAdapterDelegate { fs: Arc, http_client: Option>, node_runtime: Option, + cached_binaries: Arc>>, } impl DapAdapterDelegate { @@ -1245,11 +1241,13 @@ impl DapAdapterDelegate { http_client: Option>, node_runtime: Option, fs: Arc, + cached_binaries: Arc>>, ) -> Self { Self { fs, http_client, node_runtime, + cached_binaries, } } } @@ -1266,4 +1264,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { fn fs(&self) -> Arc { self.fs.clone() } + + fn cached_binaries(&self) -> Arc>> { + self.cached_binaries.clone() + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 30e1d415139f29..f363e1d6dde6f8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -624,6 +624,7 @@ impl Project { cx, ) }); + cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); let buffer_store = cx .new_model(|cx| BufferStore::local(worktree_store.clone(), dap_store.clone(), cx)); @@ -663,8 +664,6 @@ impl Project { cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); - cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); - let lsp_store = cx.new_model(|cx| { LspStore::new_local( buffer_store.clone(), @@ -2354,6 +2353,10 @@ impl Project { message: message.clone(), }); } + DapStoreEvent::Notification(message) => cx.emit(Event::Toast { + notification_id: "dap".into(), + message: message.clone(), + }), } } diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 93340729df2b45..5fe9b055ab3828 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -1,8 +1,7 @@ -use std::path::{Path, PathBuf}; - use crate::ResultExt; use async_fs as fs; use futures_lite::StreamExt; +use std::path::{Path, PathBuf}; /// Removes all files and directories matching the given predicate pub async fn remove_matching(dir: &Path, predicate: F) @@ -27,6 +26,25 @@ where } } +pub async fn collect_matching(dir: &Path, predicate: F) -> Vec +where + F: Fn(&Path) -> bool, +{ + let mut matching = vec![]; + + 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() { + if predicate(entry.path().as_path()) { + matching.push(entry.path()); + } + } + } + } + + matching +} + pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option where F: Fn(&str) -> bool, @@ -36,8 +54,11 @@ where 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("")) { + if let Some(file_name) = entry_path + .file_name() + .and_then(|file_name| file_name.to_str()) + { + if predicate(file_name) { return Some(entry_path); } }