fix: classify official download failures

Distinguish terminal HTTP errors from retryable CDN/network failures, record quarantined resources, and report failed download progress.

Fixes #7
This commit is contained in:
2026-07-14 01:30:12 +08:00
parent 25c2c4d40f
commit 4192d7ee4c
15 changed files with 1034 additions and 195 deletions
+19 -41
View File
@@ -5,9 +5,11 @@
//! discover the latest Windows client package and manifest, while resource
//! updates still come from the game client's server-info flow.
use crate::curl_transfer::run_curl_with_retry as run_curl_command_with_retry;
use bat_adapters::official::launcher::{YostarJpLauncherManifestFile, YOSTAR_JP_GAME_TAG};
use md5::{Digest, Md5};
use serde::Deserialize;
use std::path::Path;
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -165,8 +167,8 @@ impl OfficialLauncherBootstrapService {
return Err(format!("拒绝拉取非官方启动器 manifest URL{manifest_url}"));
}
let output = self.run_curl_with_retry(
|| {
let output = self
.run_curl_with_retry(manifest_url, || {
let mut command = Command::new(&self.curl_command);
command
.arg("--fail")
@@ -176,15 +178,8 @@ impl OfficialLauncherBootstrapService {
.arg("--url")
.arg(manifest_url);
command
},
|output| {
let stderr = String::from_utf8_lossy(&output.stderr);
format!(
"curl 拉取启动器 manifest 失败 {manifest_url}{}",
stderr.trim()
)
},
)?;
})
.map_err(|error| format!("curl 拉取启动器 manifest 失败 {manifest_url}{error}"))?;
let manifest: YostarJpLauncherRemoteManifest = serde_json::from_slice(&output.stdout)
.map_err(|error| format!("解析官方启动器 manifest 失败:{error}"))?;
@@ -222,8 +217,8 @@ impl OfficialLauncherBootstrapService {
let url = launcher_api_url(path)?;
let authorization = launcher_authorization_header(&self.launcher_version, "", None)?;
let output = self.run_curl_with_retry(
|| {
let output = self
.run_curl_with_retry(&url, || {
let mut command = Command::new(&self.curl_command);
command
.arg("--fail")
@@ -237,12 +232,8 @@ impl OfficialLauncherBootstrapService {
.arg("--url")
.arg(&url);
command
},
|output| {
let stderr = String::from_utf8_lossy(&output.stderr);
format!("curl 拉取失败 {url}{}", stderr.trim())
},
)?;
})
.map_err(|error| format!("curl 拉取失败 {url}{error}"))?;
let envelope: LauncherEnvelope = serde_json::from_slice(&output.stdout)
.map_err(|error| format!("解析官方启动器 API 响应失败:{error}"))?;
@@ -264,30 +255,17 @@ impl OfficialLauncherBootstrapService {
fn run_curl_with_retry(
&self,
url: &str,
mut build_command: impl FnMut() -> Command,
failure_message: impl Fn(&Output) -> String,
) -> Result<Output, String> {
let attempts = self.retry_attempts.max(1);
let mut last_error = None;
for attempt in 1..=attempts {
match build_command().output() {
Ok(output) if output.status.success() => return Ok(output),
Ok(output) => {
last_error = Some(format!(
"{attempt}/{attempts} 次尝试失败:{}",
failure_message(&output)
));
}
Err(error) => {
last_error = Some(format!(
"{attempt}/{attempts} 次尝试失败:启动 curl 命令失败 {}{error}",
self.curl_command
));
}
}
}
Err(last_error.unwrap_or_else(|| format!("curl 命令未执行:{}", self.curl_command)))
run_curl_command_with_retry(
Path::new(&self.curl_command),
url,
None,
self.retry_attempts,
&mut build_command,
)
.map_err(|error| error.to_string())
}
}