fix(m204): 补「扫描中/还没扫描过/正在取消」三格种子 + try_lock 说明对得上代码
视觉复审 r1 判 p2-1items:六张举证图里「取消」全程是禁用态,「正在扫描… 3/7」 一格都没拍,而 README 却写成「两态」——证据覆盖不到结论(与 M201 同一个坑)。 本提交补种子与拆文件,不出产品行为改动。 - `pages/settings/java/seed.rs`:新增 `scanning` / `unscanned` / `cancelling` 三档。 三档都把快照写进 `ScanSlot`(新增 `ScanSlot::seed_report`)而不只改 `State.scanning`: `clock.advance` 每拍用槽里的快照重算 `scanning`,只改 State 的话第一拍就被推回「没在扫」。 - `pages/settings/java/scan.rs`:`ScanReport` / `Job` / `ScanSlot` / `run_staged` / `rows` 从 `store.rs` 拆出(加 `seed_report` 之后那个文件 408 行,越过 400 行硬线); `store.rs` 用 `pub use` 再导出 `ScanReport` / `ScanSlot`,既有路径一个都没改。 - `app/view.rs`:`note_screenshot_group` 的锁从 `lock()` 换成 `try_lock()`——代码复审 r1 指出 注释写着「拿不到锁就不打、不能把界面帧卡在锁上」,而实现用的是阻塞 `lock()`,只兜住中毒 不兜住等待。选 try_lock 而不是改注释:注释承诺的保证应由代码提供,不能靠「今天只有一个 调用点、锁里只有一次比较」这种会随改动失效的前提。 - `pages/settings/java/tests/cmdline.rs`:新增 `the_scan_seeds_cover_the_running_states`, 逐档钉 `State.scanning` 与 `scan.report().running`(后者才是「心跳下一拍会不会把它推开」)。
This commit is contained in:
1 parent
92ab3d4ddf
commit
8a050c4402
6 files changed
+290
-157
No files matched your search
@@ -49,14 +49,20 @@ static SHOT_GROUP_LOGGED: std::sync::Mutex<Option<(usize, String)>> = std::sync:
|
|||||||
///
|
///
|
||||||
/// `App::view` 每帧都调用;每帧一行会把 `launcher.log` 刷满,反而没人能从中找到那一行。
|
/// `App::view` 每帧都调用;每帧一行会把 `launcher.log` 刷满,反而没人能从中找到那一行。
|
||||||
/// 判据是(组序号,主题名)这一对——同一次截图进程里最多两行(首帧 + 主题切一次)。
|
/// 判据是(组序号,主题名)这一对——同一次截图进程里最多两行(首帧 + 主题切一次)。
|
||||||
/// 拿不到锁(另一个线程正在写)就**不打**:身份日志宁可少一行,也不能把界面帧卡在锁上。
|
///
|
||||||
|
/// ## 为什么是 `try_lock` 而不是 `lock`
|
||||||
|
///
|
||||||
|
/// 这里在 UI 线程的每帧路径上。`try_lock` 拿不到锁就**直接返回**(这一帧不打,下一帧还会再来,
|
||||||
|
/// 身份行只是晚一拍、不会丢),于是「界面帧永远不会被这把锁挡住」是**代码保证的**,不是靠
|
||||||
|
/// 「今天只有一个调用点、锁里只有一次比较」这种会随改动失效的前提。M204 r1 复审把原先那句
|
||||||
|
/// 「拿不到锁就不打」和当时用的阻塞 `lock()` 对上了:注释承诺的保证必须由代码提供。
|
||||||
///
|
///
|
||||||
/// **非截图通道一律不打**:交互模式下用户每点一次左列就多一行日志,没有价值。
|
/// **非截图通道一律不打**:交互模式下用户每点一次左列就多一行日志,没有价值。
|
||||||
fn note_screenshot_group(screenshot: bool, route: &Route, group: usize, theme: &str) {
|
fn note_screenshot_group(screenshot: bool, route: &Route, group: usize, theme: &str) {
|
||||||
if !screenshot {
|
if !screenshot {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Ok(mut last) = SHOT_GROUP_LOGGED.lock() else {
|
let Ok(mut last) = SHOT_GROUP_LOGGED.try_lock() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let seen = (group, theme.to_string());
|
let seen = (group, theme.to_string());
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub(crate) mod prefs;
|
|||||||
pub(crate) mod prefs_store;
|
pub(crate) mod prefs_store;
|
||||||
mod prefs_view;
|
mod prefs_view;
|
||||||
mod row;
|
mod row;
|
||||||
|
pub(crate) mod scan;
|
||||||
mod seed;
|
mod seed;
|
||||||
mod state;
|
mod state;
|
||||||
pub(crate) mod store;
|
pub(crate) mod store;
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
//! 「Java」分组的**扫描槽与生产作业**(M204 r1 从 `store.rs` 拆出:那个文件加了
|
||||||
|
//! `ScanSlot::seed_report` 之后 408 行,越过 400 行硬线)。
|
||||||
|
//!
|
||||||
|
//! 两件事:
|
||||||
|
//!
|
||||||
|
//! | 东西 | 为什么在这 |
|
||||||
|
//! |---|---|
|
||||||
|
//! | [`ScanSlot`] | 后台线程写、界面每拍读的快照 + 取消位 + 「线程还活着」。它是**每份 `State` 一个**的槽(不是进程级 static),理由见结构体文档 |
|
||||||
|
//! | [`run_staged`] / [`rows`] | 生产作业:真扫盘 + 逐个探测(超时与取消都在 `core::game::java::staged::scan` 里),并把 `JavaRuntime` 翻成页面行 |
|
||||||
|
//!
|
||||||
|
//! `store.rs` 用 `pub use` 把 [`Job`] / [`ScanReport`] / [`ScanSlot`] 再导出,所以
|
||||||
|
//! `pages::settings::java::store::ScanSlot` 这类既有路径**一个都不用改**。
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use unlucky_core::game::java::{staged, JavaRuntime};
|
||||||
|
|
||||||
|
use super::state::RuntimeRow;
|
||||||
|
|
||||||
|
/// 扫描结果快照(后台线程写、界面读)。
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct ScanReport {
|
||||||
|
/// 扫描线程**还活着**(取消之后到它真退出之前仍是 `true`——这一段界面上叫「正在取消…」)
|
||||||
|
pub running: bool,
|
||||||
|
/// 已探测
|
||||||
|
pub done: usize,
|
||||||
|
/// 候选总数(`0` = 候选表还在收集,界面不显示 `0/0`)
|
||||||
|
pub total: usize,
|
||||||
|
/// 至少完整扫完过一次
|
||||||
|
pub finished: bool,
|
||||||
|
/// 用户按过取消(线程还活着时是 `true`)
|
||||||
|
pub cancelled: bool,
|
||||||
|
/// 扫描结论(`finished` 且没被取消时有效)
|
||||||
|
pub runtimes: Vec<RuntimeRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一次扫描的作业(生产实现是 [`run_staged`];测试注入可控的假作业)。
|
||||||
|
pub type Job =
|
||||||
|
fn(&[staged::Candidate], &dyn Fn() -> bool, &mut dyn FnMut(usize, usize)) -> Vec<RuntimeRow>;
|
||||||
|
|
||||||
|
/// 一次扫描的**状态槽**:快照 + 取消位 + 「线程还活着」。
|
||||||
|
///
|
||||||
|
/// 为什么不是进程级 static(M175 r1 复审 P2-5 的附带项):全局状态有两个坏处——
|
||||||
|
/// ①候选收集要在 UI 线程上抢同一把锁;②单测之间互相污染(一个用例起的扫描会让另一个用例
|
||||||
|
/// 看到「变了」)。挂在 `State` 上之后,每个 `App` / 每个用例都有一份自己的槽。
|
||||||
|
///
|
||||||
|
/// 内部是 `Arc`:`State` 会被整体替换(hydrate),而扫描线程要活得比某一次 `State` 赋值更久。
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ScanSlot {
|
||||||
|
inner: std::sync::Arc<ScanInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ScanInner {
|
||||||
|
report: Mutex<ScanReport>,
|
||||||
|
cancel: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScanSlot {
|
||||||
|
/// 当前快照(界面每拍读一次;锁只在这几行里持有)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn report(&self) -> ScanReport {
|
||||||
|
self.inner
|
||||||
|
.report
|
||||||
|
.lock()
|
||||||
|
.map(|report| report.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把快照**直接摆成**给定值(只给 `--screenshot` 的种子用,M204 r1)。
|
||||||
|
///
|
||||||
|
/// 为什么种子不能只写 `State.scanning`:`clock.advance` 每拍都调
|
||||||
|
/// `hydrate_java_scan` → `State::apply_scan(&self.scan.report())`,而 `scanning`
|
||||||
|
/// 是从**槽里的快照**推出来的。只改 `State` 的话,第一拍就会被推回「没在扫」,
|
||||||
|
/// 出图拍到的是另一种状态。与 `downloads::Slot::seed_report` 同一个理由、同一种做法。
|
||||||
|
///
|
||||||
|
/// 与 [`ScanSlot::start`] 的区别:这里不起线程、不摸取消位,只摆一个静态快照——
|
||||||
|
/// 出图要的是**可复现**的一帧,不是真的在扫。
|
||||||
|
pub fn seed_report(&self, report: ScanReport) {
|
||||||
|
if let Ok(mut slot) = self.inner.report.lock() {
|
||||||
|
*slot = report;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取消正在跑的扫描:置取消位 + 标 `cancelled`,**不**动 `running`。
|
||||||
|
///
|
||||||
|
/// `running` 由扫描线程自己翻——它是「线程还活着」的唯一真相。取消若也翻它,`start()` 的
|
||||||
|
/// 「已在扫」判据就失效,用户能在旧线程还卡着的时候起第二个(复审 P2-5 的第三个问题)。
|
||||||
|
pub fn cancel(&self) {
|
||||||
|
self.inner.cancel.store(true, Ordering::SeqCst);
|
||||||
|
if let Ok(mut report) = self.inner.report.lock() {
|
||||||
|
if report.running {
|
||||||
|
report.cancelled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发起一次扫描(已在扫 ⇒ 什么都不做,返回 `false`)。
|
||||||
|
pub fn start(&self, roots: Vec<PathBuf>, manual: Vec<PathBuf>, system: bool) -> bool {
|
||||||
|
self.start_with(roots, manual, system, run_staged)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 同 [`ScanSlot::start`],但扫描作业可注入(测试用:一个可控的、会一直卡住的作业)。
|
||||||
|
pub fn start_with(
|
||||||
|
&self,
|
||||||
|
roots: Vec<PathBuf>,
|
||||||
|
manual: Vec<PathBuf>,
|
||||||
|
system: bool,
|
||||||
|
job: Job,
|
||||||
|
) -> bool {
|
||||||
|
{
|
||||||
|
let Ok(mut report) = self.inner.report.lock() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if report.running {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 「占位」与「线程起来了」在同一把锁里完成:两个调用不可能同时通过上面那道判据。
|
||||||
|
*report = ScanReport {
|
||||||
|
running: true,
|
||||||
|
..ScanReport::default()
|
||||||
|
};
|
||||||
|
self.inner.cancel.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = self.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// 候选收集(注册表 / 常见目录)放在线程里:它是首屏那一拍最贵的一段,
|
||||||
|
// 放在 UI 线程上会拖慢开窗(复审 P2-5 的附带项)。
|
||||||
|
let candidates = staged::candidates(&roots, &manual, system);
|
||||||
|
if let Ok(mut report) = slot.inner.report.lock() {
|
||||||
|
report.total = candidates.len();
|
||||||
|
}
|
||||||
|
let cancelled = || slot.inner.cancel.load(Ordering::SeqCst);
|
||||||
|
let mut progress = |done: usize, _total: usize| {
|
||||||
|
if let Ok(mut report) = slot.inner.report.lock() {
|
||||||
|
report.done = done;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let runtimes = job(&candidates, &cancelled, &mut progress);
|
||||||
|
if let Ok(mut report) = slot.inner.report.lock() {
|
||||||
|
report.running = false;
|
||||||
|
report.done = candidates.len();
|
||||||
|
if !report.cancelled {
|
||||||
|
report.finished = true;
|
||||||
|
report.runtimes = runtimes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生产扫描作业:真扫盘 + 逐个探测(超时与取消都在 `staged::scan` 里)。
|
||||||
|
fn run_staged(
|
||||||
|
candidates: &[staged::Candidate],
|
||||||
|
cancelled: &dyn Fn() -> bool,
|
||||||
|
progress: &mut dyn FnMut(usize, usize),
|
||||||
|
) -> Vec<RuntimeRow> {
|
||||||
|
let found = staged::scan(
|
||||||
|
candidates,
|
||||||
|
|done, total| {
|
||||||
|
progress(done, total);
|
||||||
|
true
|
||||||
|
},
|
||||||
|
cancelled,
|
||||||
|
);
|
||||||
|
rows(&found, candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `JavaRuntime` → 页面行(来源从候选表里按路径回查:`JavaRuntime.origin` 分不出「手动」)。
|
||||||
|
fn rows(found: &[JavaRuntime], candidates: &[staged::Candidate]) -> Vec<RuntimeRow> {
|
||||||
|
found
|
||||||
|
.iter()
|
||||||
|
.map(|runtime| RuntimeRow {
|
||||||
|
binary: runtime.binary.display().to_string(),
|
||||||
|
version: runtime.version.clone().unwrap_or_default(),
|
||||||
|
major: runtime.major,
|
||||||
|
vendor: runtime.vendor.clone(),
|
||||||
|
source: candidates
|
||||||
|
.iter()
|
||||||
|
.find(|candidate| candidate.binary == runtime.binary)
|
||||||
|
.map_or("system", |candidate| candidate.source.id()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -12,6 +12,11 @@
|
|||||||
//! 与 `ULL_AI_STATE` / `ULL_UPDATE_STATE` / `ULL_FONT_STATUS` 同一族取证开关:只影响这一次进程,
|
//! 与 `ULL_AI_STATE` / `ULL_UPDATE_STATE` / `ULL_FONT_STATUS` 同一族取证开关:只影响这一次进程,
|
||||||
//! 产品路径不设它。种子里的路径是**写死的示例路径**(真实机器上没有),所以它只在出图通道里出现;
|
//! 产品路径不设它。种子里的路径是**写死的示例路径**(真实机器上没有),所以它只在出图通道里出现;
|
||||||
//! 产品路径的真值一律来自 `core::game::java::staged` 的真扫盘。
|
//! 产品路径的真值一律来自 `core::game::java::staged` 的真扫盘。
|
||||||
|
//!
|
||||||
|
//! M204 r1(视觉复审 p2-1)又补了三档:`scanning`(正在扫描… 3/7,「取消」可点)/ `unscanned`
|
||||||
|
//! (还没扫描过)/ `cancelling`(正在取消…,两颗按钮都灰)。起因是六张举证图里「取消」全程是
|
||||||
|
//! 禁用态,「扫描中」一格都没拍,而 README 却写成「两态」——**证据覆盖不到结论**(M201 同一个坑)。
|
||||||
|
//! 这三档都把快照写进 `ScanSlot`(`store::ScanSlot::seed_report`),理由见那个函数的文档。
|
||||||
|
|
||||||
use super::state::{Context, Layer, Notice, Refusal, Requirement, RuntimeRow, State};
|
use super::state::{Context, Layer, Notice, Refusal, Requirement, RuntimeRow, State};
|
||||||
|
|
||||||
@@ -74,6 +79,53 @@ pub fn build(raw: &str) -> Option<State> {
|
|||||||
seeded.notice = Some(Notice::Refused(Refusal::MemoryRange));
|
seeded.notice = Some(Notice::Refused(Refusal::MemoryRange));
|
||||||
return Some(seeded);
|
return Some(seeded);
|
||||||
}
|
}
|
||||||
|
// ── M204 r1(视觉复审 p2-1):扫描的三格。复审指出六张图里「取消」全程是禁用态、
|
||||||
|
// 「正在扫描… 3/7」一张没有,而 README 却写成了「两态」——这里补上能画出来的三格
|
||||||
|
// (扫描中 / 还没扫描过 / 正在取消…;「扫描已取消…」那一句仍没有图,见 README §3)。
|
||||||
|
//
|
||||||
|
// 三档都把快照**写进槽**(`ScanSlot::seed_report`)而不只是改 `State.scanning`:
|
||||||
|
// 心跳每拍用槽里的快照重算 `scanning`,只改 `State` 的话第一拍就被推回「没在扫」。
|
||||||
|
"scanning" => {
|
||||||
|
let mut seeded = build("two")?;
|
||||||
|
// 扫描中:列表还没有结论(`scanned` 仍是 false),状态行画「正在扫描… 3/7」,
|
||||||
|
// 「立即扫描」灰掉、「取消」可点(`view.rs` 的 `scanning && !cancelled`)。
|
||||||
|
seeded.scanned = false;
|
||||||
|
seeded.runtimes.clear();
|
||||||
|
seeded.cancelled = false;
|
||||||
|
seeded.scanning = Some(super::state::Progress { done: 3, total: 7 });
|
||||||
|
seeded.scan.seed_report(super::store::ScanReport {
|
||||||
|
running: true,
|
||||||
|
done: 3,
|
||||||
|
total: 7,
|
||||||
|
finished: false,
|
||||||
|
cancelled: false,
|
||||||
|
runtimes: Vec::new(),
|
||||||
|
});
|
||||||
|
return Some(seeded);
|
||||||
|
}
|
||||||
|
"unscanned" => {
|
||||||
|
// 还没扫描过:状态行「还没扫描过」,列表处画「列表还是空的…」(`view.rs:110-113`)。
|
||||||
|
let mut seeded = build("empty")?;
|
||||||
|
seeded.scanned = false;
|
||||||
|
seeded.scanning = None;
|
||||||
|
seeded.cancelled = false;
|
||||||
|
seeded.scan.seed_report(super::store::ScanReport::default());
|
||||||
|
return Some(seeded);
|
||||||
|
}
|
||||||
|
"cancelling" => {
|
||||||
|
// 取消已按下、线程还没退出:状态行「正在取消…」、两颗按钮都灰(复审点名的另一格)。
|
||||||
|
let mut seeded = build("scanning")?;
|
||||||
|
seeded.cancelled = true;
|
||||||
|
seeded.scan.seed_report(super::store::ScanReport {
|
||||||
|
running: true,
|
||||||
|
done: 3,
|
||||||
|
total: 7,
|
||||||
|
finished: false,
|
||||||
|
cancelled: true,
|
||||||
|
runtimes: Vec::new(),
|
||||||
|
});
|
||||||
|
return Some(seeded);
|
||||||
|
}
|
||||||
// ── M197:「下载一个 JDK」的三态
|
// ── M197:「下载一个 JDK」的三态
|
||||||
"download" => {
|
"download" => {
|
||||||
let mut seeded = build("two")?;
|
let mut seeded = build("two")?;
|
||||||
|
|||||||
@@ -32,168 +32,18 @@
|
|||||||
//! 代价远大于收益;而 `core::game::java::manual` 的读写是纯文本、一行一个,坏了顶多丢名单。
|
//! 代价远大于收益;而 `core::game::java::manual` 的读写是纯文本、一行一个,坏了顶多丢名单。
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use unlucky_core::game::java::{discover, manual, staged, JavaRuntime};
|
use unlucky_core::game::java::{discover, manual, JavaRuntime};
|
||||||
use unlucky_core::platform::paths::{Platform, ProcessEnv};
|
use unlucky_core::platform::paths::{Platform, ProcessEnv};
|
||||||
use unlucky_core::setting::keys::launcher_schema;
|
use unlucky_core::setting::keys::launcher_schema;
|
||||||
use unlucky_core::setting::store::{SaveOutcome, SettingStore};
|
use unlucky_core::setting::store::{SaveOutcome, SettingStore};
|
||||||
|
|
||||||
use super::state::{Refusal, RuntimeRow, Snapshot, State, Unwritable};
|
use super::state::{Refusal, RuntimeRow, Snapshot, State, Unwritable};
|
||||||
|
|
||||||
/// 扫描结果快照(后台线程写、界面读)。
|
// M204 r1:扫描槽与生产作业搬到了 [`super::scan`](本文件加 `ScanSlot::seed_report` 之后
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
// 408 行,越过 400 行硬线)。这里再导出,`store::ScanSlot` / `store::ScanReport` 这些既有路径
|
||||||
pub struct ScanReport {
|
// **一个都不用改**;`Job` 只被 `scan` 内部用,不外抛(外抛会让本文件多一条 unused 警告)。
|
||||||
/// 扫描线程**还活着**(取消之后到它真退出之前仍是 `true`——这一段界面上叫「正在取消…」)
|
pub use super::scan::{ScanReport, ScanSlot};
|
||||||
pub running: bool,
|
|
||||||
/// 已探测
|
|
||||||
pub done: usize,
|
|
||||||
/// 候选总数(`0` = 候选表还在收集,界面不显示 `0/0`)
|
|
||||||
pub total: usize,
|
|
||||||
/// 至少完整扫完过一次
|
|
||||||
pub finished: bool,
|
|
||||||
/// 用户按过取消(线程还活着时是 `true`)
|
|
||||||
pub cancelled: bool,
|
|
||||||
/// 扫描结论(`finished` 且没被取消时有效)
|
|
||||||
pub runtimes: Vec<RuntimeRow>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 一次扫描的作业(生产实现是 [`run_staged`];测试注入可控的假作业)。
|
|
||||||
pub type Job =
|
|
||||||
fn(&[staged::Candidate], &dyn Fn() -> bool, &mut dyn FnMut(usize, usize)) -> Vec<RuntimeRow>;
|
|
||||||
|
|
||||||
/// 一次扫描的**状态槽**:快照 + 取消位 + 「线程还活着」。
|
|
||||||
///
|
|
||||||
/// 为什么不是进程级 static(M175 r1 复审 P2-5 的附带项):全局状态有两个坏处——
|
|
||||||
/// ①候选收集要在 UI 线程上抢同一把锁;②单测之间互相污染(一个用例起的扫描会让另一个用例
|
|
||||||
/// 看到「变了」)。挂在 `State` 上之后,每个 `App` / 每个用例都有一份自己的槽。
|
|
||||||
///
|
|
||||||
/// 内部是 `Arc`:`State` 会被整体替换(hydrate),而扫描线程要活得比某一次 `State` 赋值更久。
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct ScanSlot {
|
|
||||||
inner: std::sync::Arc<ScanInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct ScanInner {
|
|
||||||
report: Mutex<ScanReport>,
|
|
||||||
cancel: AtomicBool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScanSlot {
|
|
||||||
/// 当前快照(界面每拍读一次;锁只在这几行里持有)。
|
|
||||||
#[must_use]
|
|
||||||
pub fn report(&self) -> ScanReport {
|
|
||||||
self.inner
|
|
||||||
.report
|
|
||||||
.lock()
|
|
||||||
.map(|report| report.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 取消正在跑的扫描:置取消位 + 标 `cancelled`,**不**动 `running`。
|
|
||||||
///
|
|
||||||
/// `running` 由扫描线程自己翻——它是「线程还活着」的唯一真相。取消若也翻它,`start()` 的
|
|
||||||
/// 「已在扫」判据就失效,用户能在旧线程还卡着的时候起第二个(复审 P2-5 的第三个问题)。
|
|
||||||
pub fn cancel(&self) {
|
|
||||||
self.inner.cancel.store(true, Ordering::SeqCst);
|
|
||||||
if let Ok(mut report) = self.inner.report.lock() {
|
|
||||||
if report.running {
|
|
||||||
report.cancelled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 发起一次扫描(已在扫 ⇒ 什么都不做,返回 `false`)。
|
|
||||||
pub fn start(&self, roots: Vec<PathBuf>, manual: Vec<PathBuf>, system: bool) -> bool {
|
|
||||||
self.start_with(roots, manual, system, run_staged)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 同 [`ScanSlot::start`],但扫描作业可注入(测试用:一个可控的、会一直卡住的作业)。
|
|
||||||
pub fn start_with(
|
|
||||||
&self,
|
|
||||||
roots: Vec<PathBuf>,
|
|
||||||
manual: Vec<PathBuf>,
|
|
||||||
system: bool,
|
|
||||||
job: Job,
|
|
||||||
) -> bool {
|
|
||||||
{
|
|
||||||
let Ok(mut report) = self.inner.report.lock() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if report.running {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 「占位」与「线程起来了」在同一把锁里完成:两个调用不可能同时通过上面那道判据。
|
|
||||||
*report = ScanReport {
|
|
||||||
running: true,
|
|
||||||
..ScanReport::default()
|
|
||||||
};
|
|
||||||
self.inner.cancel.store(false, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
let slot = self.clone();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
// 候选收集(注册表 / 常见目录)放在线程里:它是首屏那一拍最贵的一段,
|
|
||||||
// 放在 UI 线程上会拖慢开窗(复审 P2-5 的附带项)。
|
|
||||||
let candidates = staged::candidates(&roots, &manual, system);
|
|
||||||
if let Ok(mut report) = slot.inner.report.lock() {
|
|
||||||
report.total = candidates.len();
|
|
||||||
}
|
|
||||||
let cancelled = || slot.inner.cancel.load(Ordering::SeqCst);
|
|
||||||
let mut progress = |done: usize, _total: usize| {
|
|
||||||
if let Ok(mut report) = slot.inner.report.lock() {
|
|
||||||
report.done = done;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let runtimes = job(&candidates, &cancelled, &mut progress);
|
|
||||||
if let Ok(mut report) = slot.inner.report.lock() {
|
|
||||||
report.running = false;
|
|
||||||
report.done = candidates.len();
|
|
||||||
if !report.cancelled {
|
|
||||||
report.finished = true;
|
|
||||||
report.runtimes = runtimes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生产扫描作业:真扫盘 + 逐个探测(超时与取消都在 `staged::scan` 里)。
|
|
||||||
fn run_staged(
|
|
||||||
candidates: &[staged::Candidate],
|
|
||||||
cancelled: &dyn Fn() -> bool,
|
|
||||||
progress: &mut dyn FnMut(usize, usize),
|
|
||||||
) -> Vec<RuntimeRow> {
|
|
||||||
let found = staged::scan(
|
|
||||||
candidates,
|
|
||||||
|done, total| {
|
|
||||||
progress(done, total);
|
|
||||||
true
|
|
||||||
},
|
|
||||||
cancelled,
|
|
||||||
);
|
|
||||||
rows(&found, candidates)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `JavaRuntime` → 页面行(来源从候选表里按路径回查:`JavaRuntime.origin` 分不出「手动」)。
|
|
||||||
fn rows(found: &[JavaRuntime], candidates: &[staged::Candidate]) -> Vec<RuntimeRow> {
|
|
||||||
found
|
|
||||||
.iter()
|
|
||||||
.map(|runtime| RuntimeRow {
|
|
||||||
binary: runtime.binary.display().to_string(),
|
|
||||||
version: runtime.version.clone().unwrap_or_default(),
|
|
||||||
major: runtime.major,
|
|
||||||
vendor: runtime.vendor.clone(),
|
|
||||||
source: candidates
|
|
||||||
.iter()
|
|
||||||
.find(|candidate| candidate.binary == runtime.binary)
|
|
||||||
.map_or("system", |candidate| candidate.source.id()),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 该不该**自动**发起一次扫描(首屏 / 配置文件刚被外部改过时)。
|
/// 该不该**自动**发起一次扫描(首屏 / 配置文件刚被外部改过时)。
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -220,6 +220,42 @@ fn the_screenshot_seeds_cover_the_three_states() {
|
|||||||
assert!(State::screenshot_seed("nonsense").is_none());
|
assert!(State::screenshot_seed("nonsense").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M204 r1:扫描的三档(扫描中 / 还没扫描过 / 正在取消)——视觉复审 p2-1 要的正是这三格。
|
||||||
|
///
|
||||||
|
/// 每档的判据都落在**界面真的会读的那两个字段**上:`State.scanning`(状态行画什么)与
|
||||||
|
/// `State.scan.report().running`(心跳下一拍会不会把 `scanning` 推开)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `"scanning"` 档里的 `scan.seed_report(...)` 换成不调 ⇒ `scanning` 在心跳第一拍
|
||||||
|
/// 就被推回 `None`(`apply_scan` 用槽里的快照重算)⇒ 第二段红。这也正是这两档必须写槽、
|
||||||
|
/// 不能只改 `State` 的理由。
|
||||||
|
#[test]
|
||||||
|
fn the_scan_seeds_cover_the_running_states() {
|
||||||
|
let scanning = State::screenshot_seed("scanning").expect("scanning 档");
|
||||||
|
assert_eq!(
|
||||||
|
scanning.scanning,
|
||||||
|
Some(super::super::state::Progress { done: 3, total: 7 }),
|
||||||
|
"状态行要画「正在扫描… 3/7」"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
scanning.scan.report().running,
|
||||||
|
"槽里的快照也得是「在扫」,否则下一拍就把 scanning 推开"
|
||||||
|
);
|
||||||
|
assert!(!scanning.scanned, "还没扫完过");
|
||||||
|
assert!(!scanning.cancelled, "没按过取消 ⇒「取消」这颗按钮可点");
|
||||||
|
assert!(scanning.runtimes.is_empty(), "扫描中列表还没有结论");
|
||||||
|
|
||||||
|
let cancelling = State::screenshot_seed("cancelling").expect("cancelling 档");
|
||||||
|
assert!(cancelling.cancelled && cancelling.scan.report().running);
|
||||||
|
assert!(
|
||||||
|
cancelling.scanning.is_some(),
|
||||||
|
"线程还没退出 ⇒ 状态行仍是活动态(画的是「正在取消…」)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let unscanned = State::screenshot_seed("unscanned").expect("unscanned 档");
|
||||||
|
assert!(unscanned.scanning.is_none() && !unscanned.scanned);
|
||||||
|
assert!(!unscanned.scan.report().running, "这一档没有扫描在跑");
|
||||||
|
}
|
||||||
|
|
||||||
/// M197 的三档:版本选择列表 / 下载中 / 下载失败——出图靠它们,所以每档都要有**可断言的特征**。
|
/// M197 的三档:版本选择列表 / 下载中 / 下载失败——出图靠它们,所以每档都要有**可断言的特征**。
|
||||||
///
|
///
|
||||||
/// 变异证明:把 `seed::build` 里 `"downloading"` 那一档的 `seed_report` 换成 `Report::default()`
|
/// 变异证明:把 `seed::build` 里 `"downloading"` 那一档的 `seed_report` 换成 `Report::default()`
|
||||||
|
|||||||
Reference in new issue
Block a user