From 09cbf56ebc2d897fbd458a9c977f01e8356f70a1 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 12 Aug 2026 17:45:36 +0200 Subject: [PATCH] pull: fix hang caused by blocking git2 call and redundant Launchpad checks --- src/package_info.rs | 125 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/src/package_info.rs b/src/package_info.rs index 741192c..2a651d5 100644 --- a/src/package_info.rs +++ b/src/package_info.rs @@ -19,7 +19,7 @@ pub fn ppa_to_base_url(user: &str, name: &str) -> String { format!("http://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name) } -async fn check_launchpad_repo(package: &str) -> Result, Box> { +fn check_launchpad_repo_sync(package: &str) -> Result, String> { let url = format!("https://git.launchpad.net/ubuntu/+source/{}", package); // Use libgit2 to check if the remote repository exists @@ -33,6 +33,31 @@ async fn check_launchpad_repo(package: &str) -> Result, Box Result, Box> { + let package_owned = package.to_string(); + let package_for_err = package_owned.clone(); + let result = tokio::time::timeout( + std::time::Duration::from_secs(15), + tokio::task::spawn_blocking(move || check_launchpad_repo_sync(&package_owned)), + ) + .await + .map_err(|_| { + format!( + "Timed out checking Launchpad repository for '{}'", + package_for_err + ) + })? + .map_err(|e| format!("Launchpad check task failed: {}", e))?; + result.map_err(|e| -> Box { e.into() }) +} + /// A File used in a source package #[derive(Debug, Clone)] pub struct FileEntry { @@ -245,24 +270,24 @@ fn parse_sources( } /// Get package information from a package, distribution series, and pocket +/// +/// `launchpad_vcs` is a pre-computed Launchpad VCS URL (if any), checked once +/// by the caller so that the blocking git2 connection test is not repeated for +/// every series/pocket combination. async fn get( package_name: &str, series: &str, pocket: &str, version: Option<&str>, base_url: Option<&str>, + launchpad_vcs: Option, ) -> Result> { let dist = crate::distro_info::get_dist_from_series(series).await?; - // Handle Ubuntu case: Vcs-Git does not usually point to Launchpad but Salsa - // We need to check manually if there is a launchpad repository for the package - let mut preferred_vcs = None; - if dist == "ubuntu" - && let Some(lp_url) = check_launchpad_repo(package_name).await? - { - debug!("Found Launchpad URL: {}", lp_url); - preferred_vcs = Some(lp_url); - } + // Use the pre-computed Launchpad VCS URL (if any). + // This is checked once in the caller rather than per-series to avoid + // repeated blocking git2 connections. + let mut preferred_vcs = launchpad_vcs; // Determine the base URL to use (either provided PPA URL or default archive) let distro_base_url = crate::distro_info::get_base_url(&dist)?; @@ -340,6 +365,7 @@ async fn find_package( pocket: &str, version: Option<&str>, base_url: Option<&str>, + launchpad_vcs: Option, progress: ProgressCallback<'_>, ) -> Result> { let series_list = crate::distro_info::get_ordered_series_name(dist).await?; @@ -356,7 +382,16 @@ async fn find_package( }; for p in pockets { - match get(package_name, series, &p, version, base_url).await { + match get( + package_name, + series, + &p, + version, + base_url, + launchpad_vcs.clone(), + ) + .await + { Ok(info) => { if i > 0 { let location = if p.is_empty() { @@ -410,6 +445,41 @@ pub async fn lookup( base_url: Option<&str>, progress: ProgressCallback<'_>, ) -> Result> { + // Resolve the distribution early so we can check Launchpad once + let resolved_dist = dist.unwrap_or_else(|| + // Use auto-detection to see if current distro is ubuntu, or fallback to debian by default + if std::process::Command::new("lsb_release").arg("-i").arg("-s").output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_lowercase()).unwrap_or_default() == "ubuntu" { + "ubuntu" + } else { + "debian" + } + ); + + // Check Launchpad repository once for Ubuntu packages, rather than + // once per series/pocket combination inside get(). + let launchpad_vcs = if resolved_dist == "ubuntu" && base_url.is_none() { + if let Some(cb) = progress { + cb("", &format!("Checking Launchpad for {}...", package), 0, 0); + } + match check_launchpad_repo(package).await { + Ok(Some(lp_url)) => { + debug!("Found Launchpad URL: {}", lp_url); + Some(lp_url) + } + Ok(None) => None, + Err(e) => { + debug!( + "Launchpad check failed (will use stanza Vcs-Git if available): {}", + e + ); + None + } + } + } else { + None + }; + // Obtain the package information, either directly in a series or with a search in all series let package_info = if let Some(s) = series { if let Some(cb) = progress { @@ -422,24 +492,18 @@ pub async fn lookup( } // Get the package information from that series and pocket - get(package, s, pocket, version, base_url).await? + get(package, s, pocket, version, base_url, launchpad_vcs).await? } else { - let dist = dist.unwrap_or_else(|| - // Use auto-detection to see if current distro is ubuntu, or fallback to debian by default - if std::process::Command::new("lsb_release").arg("-i").arg("-s").output() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_lowercase()).unwrap_or_default() == "ubuntu" { - "ubuntu" - } else { - "debian" - } - ); - if let Some(cb) = progress { cb( &format!( "Searching for package {} in {}...", package, - if base_url.is_none() { dist } else { "ppa" } + if base_url.is_none() { + resolved_dist + } else { + "ppa" + } ), "", 0, @@ -448,7 +512,16 @@ pub async fn lookup( } // Try to find the package in all series from that dist - find_package(package, dist, pocket, version, base_url, progress).await? + find_package( + package, + resolved_dist, + pocket, + version, + base_url, + launchpad_vcs, + progress, + ) + .await? }; Ok(package_info) @@ -512,7 +585,7 @@ Version: 1.0 #[tokio::test] async fn test_find_package_fallback() { // python2.7 is in bullseye but not above - let info = find_package("python2.7", "debian", "", None, None, None) + let info = find_package("python2.7", "debian", "", None, None, None, None) .await .unwrap(); assert_eq!(info.stanza.package, "python2.7"); @@ -522,7 +595,7 @@ Version: 1.0 #[tokio::test] async fn test_find_package_devel() { // hello is in sid - let info = find_package("hello", "debian", "", None, None, None) + let info = find_package("hello", "debian", "", None, None, None, None) .await .unwrap(); assert_eq!(info.stanza.package, "hello");