fix(java/update): 三处模块外的一锤子 GET 也上界,失败时报出卡在哪一段(M290)
`download_bytes`(M289)修掉之后,core 里还剩三处裸 `send()`,都在「拿上游文本/JSON」的 路径上:`game/java/api.rs::fetch_latest`(Adoptium latest)、`game/java/net.rs::get_text` (四家发行版的一锤子 GET)、`update/github.rs::get_candidate_text`(更新清单)。上游接了连接 就装死时它们永不返回,与 M289 修掉的是同族形态。 改动(全部复用 M289 的构造点,没有第二套超时包装): * `download/http.rs` 新增 `text_bounded`(正文按 `read_step` 的空闲判定读成 `String`,与 `bytes.rs` 的读循环同口径)与 `default_deadlines()`(取 `EngineConfig::default()` 的两段界, 不另抄秒数);`send_bounded` 的签名逐字节未动,`transfer.rs`/`chunked.rs`/`bytes.rs` 三处 调用方零改动。 * 三处一律 `send_bounded`(等响应头)+ `text_bounded`(等字节):失败时 `message` 里带 `StallReport` 的阶段(`await_headers` / `await_bytes`)与已收/预期/无进展三个数。 这三条路径只用到这两档:没有分块回收、也没有引擎许可等待,所以 `await_chunk_join` / `acquire_permit` 不适用。 * `update/github.rs` 的界取 `engine.config().deadlines()`(手里有引擎就不抄默认值)。 正常路径零改变:`fetch_latest` / `get_text` 的公开签名与错误分类一字未动(`fetch_latest` 的 404 文案与 hint 保留),`update` 的 `github_cases` 全部断言未改。 离线用例(`game::java::net::tests`,6 条;夹具 `net::probe::Hanging` 也是 M290 新增): silent 服务端 ⇒ 引擎自己在 200ms 上界内返回(外层 5s 护栏没触发),并断言 message 含 「等响应头/await_headers」;正文停住 ⇒ 「等字节/await_bytes」;200 + 真 JSON ⇒ 正文逐字节 原样(含中文)。变异已实跑:`send_bounded` 退回裸 `send()` 三处各自在外层 5s 处挂断; `text_bounded` 退回 `response.text()` ⇒ 等字节用例 5s 挂断;正文改成空串 ⇒ 正常路径用例红。 门禁:cargo check(core+cli, --all-targets)/ test --lib(892 passed)/ --test game(120)/ --test launch(91)/ --test engine_mock(17)/ clippy -D warnings / fmt --check,退出码全 0。
This commit is contained in:
1 parent
53de2a3922
commit
ad99303a95
4 files changed
+481
-59
No files matched your search
@@ -60,7 +60,9 @@ pub fn build_client(cfg: &EngineConfig) -> Result<reqwest::Client, DownloadError
|
||||
})
|
||||
}
|
||||
|
||||
/// **两处 `send()` 的唯一上界构造点**(`transfer.rs` 的单连接请求、`chunked.rs` 的分块请求)。
|
||||
/// **每一条 `send()` 的唯一上界构造点**:`transfer.rs` 的单连接请求、`chunked.rs` 的分块请求、
|
||||
/// `bytes.rs` 的 `download_bytes`,以及模块外的三条「一锤子 GET 取文本」(`game/java/api.rs`、
|
||||
/// `game/java/net.rs`、`update/github.rs`,M290)。
|
||||
///
|
||||
/// 上界只覆盖「请求发出 → 响应头到手」,正文由 [`read_step`] 的空闲判定管;为什么选
|
||||
/// 「单次请求上界」而不是「整作业总预算」、这些数字怎么来的,见 [`super::stall`] 模块头
|
||||
@@ -122,6 +124,62 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// 把整段正文读成文本(**文本/JSON 一锤子 GET** 的正文上界):与 [`read_step`] 同一处构造。
|
||||
///
|
||||
/// 为什么不是 `response.text()`:它是一次无界的整段读,上游「回了头但正文不动」时永远不返回
|
||||
/// —— 与 M289 在 `download_bytes` 里修掉的是同一种形态。这里换成流式累加 + 空闲判定,
|
||||
/// 口径与 [`super::bytes`] 的读循环逐字一致(只是落成 `String`)。
|
||||
///
|
||||
/// 只给「一次 GET 取一小段文本」用:分块/落盘作业走 [`read_step`] 自己的循环(它们要进度、
|
||||
/// 要边读边写盘、要卡内存上限),别把那条路改成经过这里。
|
||||
///
|
||||
/// 与 `reqwest::Response::text()` 的**唯一**差别:不按 `Content-Type` 的 charset 解码,一律
|
||||
/// UTF-8 lossy(声明了非 UTF-8 字符集时中文字节会变成替换符)。三条消费者(Adoptium JSON /
|
||||
/// GitHub 清单 JSON / 各镜像的目录与校验文本)上游都是 UTF-8 或纯 ASCII。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 正文读取超时 ⇒ `kind = Timeout` 的 [`DownloadError`],阶段「等字节」+ 已收/预期/无进展
|
||||
/// 三个数(`message` 里带一份,见 [`DownloadError::stalled`])。
|
||||
pub async fn text_bounded(
|
||||
response: reqwest::Response,
|
||||
url: &str,
|
||||
deadlines: Deadlines,
|
||||
) -> Result<String, DownloadError> {
|
||||
let declared = content_length(response.headers());
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut body: Vec<u8> = Vec::with_capacity(
|
||||
declared
|
||||
.and_then(|len| usize::try_from(len).ok())
|
||||
.unwrap_or(0),
|
||||
);
|
||||
loop {
|
||||
let next = read_step(
|
||||
futures_util::StreamExt::next(&mut stream),
|
||||
deadlines.read_idle,
|
||||
body.len() as u64,
|
||||
declared,
|
||||
url,
|
||||
)
|
||||
.await?;
|
||||
let Some(chunk) = next else { break };
|
||||
body.extend_from_slice(&chunk.map_err(|err| classify_reqwest(&err, url))?);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&body).into_owned())
|
||||
}
|
||||
|
||||
/// 手里没有 `EngineConfig` 的消费者用的两段界:**取 [`EngineConfig::default`],不另抄秒数**。
|
||||
///
|
||||
/// `game/java/api.rs`、`game/java/net.rs`、`update/github.rs` 的签名只收 `reqwest::Client`
|
||||
/// (`update/github.rs` 有引擎,直接用 `engine.config().deadlines()`),所以它们只能取出厂值。
|
||||
/// 今天这不产生分歧:生产里每一处 `EngineConfig` 都是 `default()`(`game/java/install/chain.rs`),
|
||||
/// `download.*` 还没接进设置页。**缺口**:将来这几个数可配置时,这三处不会跟着变——那时得给它们
|
||||
/// 加一条传配置的路(改签名会牵到 scope 外的 6 个调用点,M290 没动)。
|
||||
#[must_use]
|
||||
pub fn default_deadlines() -> Deadlines {
|
||||
EngineConfig::default().deadlines()
|
||||
}
|
||||
|
||||
/// 把 `reqwest::Error` 映射成我们的错误分类。
|
||||
#[must_use]
|
||||
pub fn classify_reqwest(err: &reqwest::Error, url: &str) -> DownloadError {
|
||||
|
||||
Reference in new issue
Block a user