use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use futures_util::stream::{FuturesUnordered, StreamExt};
use tokio::sync::Semaphore;
use crate::downloader::resolve::candidate_urls;
use crate::models::{PackageInfo, PackageSource, PrebuiltAvailability};
const PROBE_CONCURRENCY: usize = 16;
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
pub async fn probe_with_candidates(
client: &reqwest::Client,
urls: &[String],
) -> PrebuiltAvailability {
if urls.is_empty() {
return PrebuiltAvailability::Source;
}
let sem = Arc::new(Semaphore::new(PROBE_CONCURRENCY));
let cancel = Arc::new(AtomicBool::new(false));
let mut tasks: FuturesUnordered<_> = urls
.iter()
.cloned()
.map(|url| {
let sem = sem.clone();
let client = client.clone();
let cancel = cancel.clone();
async move {
let _permit = sem.acquire_owned().await.ok()?;
if cancel.load(Ordering::SeqCst) {
return None;
}
let fut = client.head(&url).send();
match tokio::time::timeout(PROBE_TIMEOUT, fut).await {
Ok(Ok(r)) if r.status().is_success() => Some(ProbeOutcome::Hit),
Ok(Ok(r)) if r.status().is_client_error() => Some(ProbeOutcome::NotFound),
Ok(Ok(_)) => Some(ProbeOutcome::ServerError),
Ok(Err(_)) => Some(ProbeOutcome::NetworkError),
Err(_) => Some(ProbeOutcome::Timeout),
}
}
})
.collect();
let mut saw_not_found = false;
let mut saw_uncertain = false;
while let Some(res) = tasks.next().await {
match res {
Some(ProbeOutcome::Hit) => {
cancel.store(true, Ordering::SeqCst);
return PrebuiltAvailability::Prebuilt;
}
Some(ProbeOutcome::NotFound) => saw_not_found = true,
Some(ProbeOutcome::ServerError)
| Some(ProbeOutcome::NetworkError)
| Some(ProbeOutcome::Timeout) => saw_uncertain = true,
None => saw_uncertain = true,
}
}
if saw_not_found && !saw_uncertain {
PrebuiltAvailability::Source
} else {
PrebuiltAvailability::Unknown
}
}
enum ProbeOutcome {
Hit,
NotFound,
ServerError,
NetworkError,
Timeout,
}
pub async fn probe_prebuilt(
client: &reqwest::Client,
name: &str,
version: &str,
) -> PrebuiltAvailability {
let targets = crate::downloader::resolve::current_targets();
if targets.is_empty() {
return PrebuiltAvailability::Unknown;
}
let repo = match crate::package::crates_api::fetch_repo_url(client, name).await {
Some(r) => r,
None => return PrebuiltAvailability::Unknown,
};
let mut name_candidates = vec![name.to_string()];
if let Some(home) = crate::package::registry::cargo_home() {
for b in crate::package::crates2::lookup_bins(&home, name) {
if !name_candidates.contains(&b) {
name_candidates.push(b);
}
}
}
if let Some((owner, repo_name)) =
crate::downloader::github_api::parse_owner_repo(&repo)
{
let token = crate::downloader::token::discover_token();
let expected = crate::downloader::resolve::expected_filenames(
&name_candidates,
version,
&targets,
);
let candidate_tags = tag_candidates(&name_candidates[0], version);
let mut hit_api = false;
for tag in &candidate_tags {
match crate::downloader::github_api::fetch_release_assets(
client,
"https://api.github.com",
&owner,
&repo_name,
tag,
token,
)
.await
{
Ok(assets) => {
hit_api = true;
if crate::downloader::github_api::match_winning_asset(&assets, &expected)
.is_some()
{
return PrebuiltAvailability::Prebuilt;
}
}
Err(crate::downloader::github_api::GithubApiError::NotFound) => {
hit_api = true;
}
Err(_) => {
break;
}
}
}
if hit_api {
return PrebuiltAvailability::Source;
}
}
let urls: Vec<String> = match candidate_urls(&name_candidates, version, &repo, &targets) {
Ok(cands) => cands.into_iter().map(|c| c.url).collect(),
Err(_) => return PrebuiltAvailability::Source,
};
probe_with_candidates(client, &urls).await
}
fn tag_candidates(pkg: &str, version: &str) -> Vec<String> {
vec![
format!("v{version}"),
version.to_string(),
format!("{pkg}-v{version}"),
format!("{pkg}-{version}"),
format!("{pkg}/v{version}"),
format!("{pkg}/{version}"),
]
}
pub async fn annotate_updates(packages: &mut [PackageInfo]) {
let client = crate::package::http_client();
let targets: Vec<(usize, String, String)> = packages
.iter()
.enumerate()
.filter(|(_, p)| matches!(p.source, PackageSource::Crates) && p.has_update())
.filter_map(|(i, p)| p.latest_version.clone().map(|v| (i, p.name.clone(), v)))
.collect();
if targets.is_empty() {
return;
}
for (i, name, ver) in targets {
let kind = probe_prebuilt(client, &name, &ver).await;
packages[i].prebuilt = Some(kind);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tag_candidates_covers_common_shapes() {
let tags = tag_candidates("ripgrep", "15.1.0");
assert!(tags.contains(&"v15.1.0".to_string()));
assert!(tags.contains(&"15.1.0".to_string()));
assert!(tags.contains(&"ripgrep-v15.1.0".to_string()));
}
}