diff --git a/crates/bin/src/args.rs b/crates/bin/src/args.rs index 845e734f..9881341e 100644 --- a/crates/bin/src/args.rs +++ b/crates/bin/src/args.rs @@ -250,8 +250,8 @@ pub struct Args { /// Disable statistics collection on popular crates. /// /// Strategy quick-install (can be disabled via --disable-strategies) collects - /// statistics of popular crates by default, by sending the crate, version and - /// target to https://warehouse-clerk-tmp.vercel.app/api/crate + /// statistics of popular crates by default, by sending the crate, version, target + /// and status to https://cargo-quickinstall-stats-server.fly.dev/record-install #[clap(help_heading = "Options", long, env = "BINSTALL_DISABLE_TELEMETRY")] pub(crate) disable_telemetry: bool, @@ -675,7 +675,7 @@ mod test { .unwrap() .to_string(); assert!( - long_help.ends_with(binstalk::QUICK_INSTALL_STATS_URL), + long_help.ends_with(binstalk::QUICKINSTALL_STATS_URL), "{}", long_help ); diff --git a/crates/binstalk-fetchers/src/common.rs b/crates/binstalk-fetchers/src/common.rs index 10272f10..ad9e1d68 100644 --- a/crates/binstalk-fetchers/src/common.rs +++ b/crates/binstalk-fetchers/src/common.rs @@ -24,6 +24,9 @@ use crate::FetchError; static WARN_RATE_LIMIT_ONCE: Once = Once::new(); static WARN_UNAUTHORIZED_ONCE: Once = Once::new(); +/// Return Ok(Some(api_artifact_url)) if exists, or Ok(None) if it doesn't. +/// +/// Caches info on all artifacts matching (repo, tag). pub(super) async fn get_gh_release_artifact_url( gh_api_client: GhApiClient, artifact: GhReleaseArtifact, @@ -54,6 +57,10 @@ pub(super) async fn get_gh_release_artifact_url( } } +/// Check if the URL exists by querying the GitHub API. +/// +/// Caches info on all artifacts matching (repo, tag). +/// /// This function returns a future where its size should be at most size of /// 2-4 pointers. pub(super) async fn does_url_exist( diff --git a/crates/binstalk-fetchers/src/quickinstall.rs b/crates/binstalk-fetchers/src/quickinstall.rs index 434f7d49..ebf6976a 100644 --- a/crates/binstalk-fetchers/src/quickinstall.rs +++ b/crates/binstalk-fetchers/src/quickinstall.rs @@ -1,7 +1,7 @@ use std::{ borrow::Cow, path::Path, - sync::{Arc, OnceLock}, + sync::{Arc, Mutex, OnceLock}, }; use binstalk_downloader::remote::Method; @@ -16,7 +16,8 @@ use crate::{ }; const BASE_URL: &str = "https://github.com/cargo-bins/cargo-quickinstall/releases/download"; -pub const QUICK_INSTALL_STATS_URL: &str = "https://warehouse-clerk-tmp.vercel.app/api/crate"; +pub const QUICKINSTALL_STATS_URL: &str = + "https://cargo-quickinstall-stats-server.fly.dev/record-install"; const QUICKINSTALL_SIGN_KEY: Cow<'static, str> = Cow::Borrowed("RWTdnnab2pAka9OdwgCMYyOE66M/BlQoFWaJ/JjwcPV+f3n24IRTj97t"); @@ -58,15 +59,39 @@ pub struct QuickInstall { gh_api_client: GhApiClient, is_supported_v: OnceCell, + data: Arc, package: String, package_url: Url, signature_url: Url, - stats_url: Url, signature_policy: SignaturePolicy, target_data: Arc, signature_verifier: OnceLock, + status: Mutex, +} + +#[derive(Debug, Clone, Copy)] +enum Status { + Start, + NotFound, + Found, + AttemptingInstall, + InvalidSignature, + InstalledFromTarball, +} + +impl Status { + fn as_str(&self) -> &'static str { + match self { + Status::Start => "start", + Status::NotFound => "not-found", + Status::Found => "found", + Status::AttemptingInstall => "attempting-install", + Status::InvalidSignature => "invalid-signature", + Status::InstalledFromTarball => "installed-from-tarball", + } + } } impl QuickInstall { @@ -116,6 +141,14 @@ impl QuickInstall { } }) } + + fn get_status(&self) -> Status { + *self.status.lock().unwrap() + } + + fn set_status(&self, status: Status) { + *self.status.lock().unwrap() = status; + } } #[async_trait::async_trait] @@ -137,6 +170,7 @@ impl super::Fetcher for QuickInstall { Arc::new(Self { client, + data, gh_api_client, is_supported_v: OnceCell::new(), @@ -144,14 +178,13 @@ impl super::Fetcher for QuickInstall { .expect("package_url is pre-generated and should never be invalid url"), signature_url: Url::parse(&format!("{url}.sig")) .expect("signature_url is pre-generated and should never be invalid url"), - stats_url: Url::parse(&format!("{QUICK_INSTALL_STATS_URL}/{package}.tar.gz",)) - .expect("stats_url is pre-generated and should never be invalid url"), package, signature_policy, target_data, signature_verifier: OnceLock::new(), + status: Mutex::new(Status::Start), }) } @@ -171,6 +204,7 @@ impl super::Fetcher for QuickInstall { .await?; if !is_found { + self.set_status(Status::NotFound); return Ok(false); } @@ -182,6 +216,7 @@ impl super::Fetcher for QuickInstall { panic!("::find is run twice"); } + self.set_status(Status::Found); Ok(true) }) } @@ -209,6 +244,7 @@ by rust officially."#, } async fn fetch_and_extract(&self, dst: &Path) -> Result { + self.set_status(Status::AttemptingInstall); let Some(verifier) = self.signature_verifier.get() else { panic!("::find has not been called yet!") }; @@ -227,8 +263,10 @@ by rust officially."#, if let Some(info) = verifier.info() { info!("Verified signature for package '{}': {info}", self.package); } + self.set_status(Status::InstalledFromTarball); Ok(files) } else { + self.set_status(Status::InvalidSignature); Err(FetchError::InvalidSignature) } } @@ -280,10 +318,20 @@ impl QuickInstall { return Ok(()); } - let url = self.stats_url.clone(); + let mut url = Url::parse(QUICKINSTALL_STATS_URL) + .expect("stats_url is pre-generated and should never be invalid url"); + url.query_pairs_mut() + .append_pair("crate", &self.data.name) + .append_pair("version", &self.data.version) + .append_pair("target", &self.target_data.target) + .append_pair( + "agent", + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), + ) + .append_pair("status", self.get_status().as_str()); debug!("Sending installation report to quickinstall ({url})"); - self.client.request(Method::HEAD, url).send(true).await?; + self.client.request(Method::POST, url).send(true).await?; Ok(()) } diff --git a/crates/binstalk-git-repo-api/src/gh_api_client.rs b/crates/binstalk-git-repo-api/src/gh_api_client.rs index 8773fa11..5b9405d9 100644 --- a/crates/binstalk-git-repo-api/src/gh_api_client.rs +++ b/crates/binstalk-git-repo-api/src/gh_api_client.rs @@ -248,6 +248,8 @@ pub struct GhReleaseArtifactUrl(Url); impl GhApiClient { /// Return `Ok(Some(api_artifact_url))` if exists. /// + /// Caches info on all artifacts matching (repo, tag). + /// /// The returned future is guaranteed to be pointer size. #[instrument(skip(self), ret(level = Level::DEBUG))] pub async fn has_release_artifact( diff --git a/crates/binstalk/src/lib.rs b/crates/binstalk/src/lib.rs index 271c0f71..83df1938 100644 --- a/crates/binstalk/src/lib.rs +++ b/crates/binstalk/src/lib.rs @@ -10,4 +10,4 @@ pub use binstalk_registry as registry; pub use binstalk_types as manifests; pub use detect_targets::{get_desired_targets, DesiredTargets, TARGET}; -pub use fetchers::QUICK_INSTALL_STATS_URL; +pub use fetchers::QUICKINSTALL_STATS_URL; diff --git a/crates/binstalk/src/ops/resolve.rs b/crates/binstalk/src/ops/resolve.rs index 59940c94..265a18b6 100644 --- a/crates/binstalk/src/ops/resolve.rs +++ b/crates/binstalk/src/ops/resolve.rs @@ -203,6 +203,7 @@ async fn resolve_inner( { Ok(bin_files) => { if !bin_files.is_empty() { + fetcher.clone().report_to_upstream(); return Ok(Resolution::Fetch(Box::new(ResolutionFetch { fetcher: fetcher.clone(), new_version: package_info.version, @@ -250,6 +251,8 @@ async fn resolve_inner( } } + // At this point, we don't know whether fallback to cargo install is allowed, or whether it will + // succeed, but things start to get convoluted when try to include that data, so this will do. if !opts.disable_telemetry { for fetcher in handles { fetcher.report_to_upstream();