feat(m175): Java 设置页接真运行时(扫描 / 手动路径 / 默认 JDK / 内存 / JVM 参数)
用户实测四条抱怨逐条对应: - 「没有自动检测本机 Java」:首屏自动扫一次盘(托管根 + JAVA_HOME + PATH + 标准安装位置 + 注册表),之后由「立即扫描」手动触发;扫描在后台线程里跑,带 done/total 进度与取消 - 「显示 Temurin 21.0.4 不知道是不是硬编码」:演示行与绊线测试一起删掉,列表来自真读盘 - 「没一个能改」:默认内存(MB,512~65536,范围与 schema 同源)与 JVM 参数可真改、真落盘、读回 - 「不能手动指定 Java 路径 / 不能改默认版本」:手动添加走严格校验(真跑 java -version), 设为默认写 java.selected_path 并给出「为什么选它」(当前实例的版本门槛 + 每行满足与否) core:java::manual(严格探测 + 厂商识别 + 名单持久化)、java::staged(候选收集 + 带进度与 取消的扫描)、java::proc(带超时且可打断的子进程执行:卡住的 java -version 会被 kill)。 i18n:catalog/settings/java.rs 55 条中英 + 金表按装配顺序插入。 复审 r1(p2-5items)全修: - P2-1 「设为默认」不再清脏:只写 java.selected_path,内存/JVM 草稿一个字节都不动 - P2-2 错误文案全部走 i18n(三档「写不进去」/「路径太长」/「名单写不进去」各自一个 key), 不再把 Rust 枚举名当人话 - P2-3 手动名单落盘有正面断言(加一条 → 从盘上新读一份 → 在) - P2-4 版本号显示到小版本(17.0.13 / 17.0.9 可区分),解析不出来时给原始版本串 - P2-5 每候选 8 秒超时并 kill、取消打断正在跑的探测、扫描槽挂在 State 上、线程活着期间 不许起第二个(界面显示「正在取消…」) 复审 r2(p2-2items)全修: - P2-1 空态不再指向不存在的「Java 页」,改成真实存在的 `ull java install 21` 命令行入口 - P2-2 `auto_scan_due` 增加 `!report.cancelled`:取消过一次之后本进程不再自动扫, 配置文件再变(存内存 / 改主题)也不会把取消无声撤销;手动「立即扫描」不受影响 - 顺带:proc 的 kill+wait 补「子进程真死了」的回归守卫(标记文件从未出现)、 core 的 Schema 错误改成页面自己的 key(不再把中文 Display 直出给英文界面)、 默认态 chip 用新的 `Chip::badge`(不再挂一条永远收不到的消息)
This commit is contained in:
1 parent
30b61e2684
commit
df331f6a60
26 files changed
+4030
-85
No files matched your search
@@ -0,0 +1,169 @@
|
||||
//! 子进程的**带超时 / 可打断**执行(M175 r1 复审 P2-5)。
|
||||
//!
|
||||
//! `Command::output()` 是「等到底」:一份放在挂死网络盘上的 `java` 会让调用它的扫描线程永远不
|
||||
//! 返回,界面上「取消」也就永远兑现不了。这里只做一件事——**要么拿到输出,要么把它杀掉**。
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// 单个候选的探测预算(`java -version` 正常几十毫秒;8 秒还没答话就是卡住了)。
|
||||
pub const PROBE_TIMEOUT: Duration = Duration::from_secs(8);
|
||||
|
||||
/// 轮询间隔(`try_wait` 的粒度):够细,取消能在一帧内生效;够粗,不烧 CPU。
|
||||
const POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
|
||||
/// 跑一个命令并等它结束,**带超时与可打断**:`cancelled()` 为真或超时就把子进程 kill 掉。
|
||||
///
|
||||
/// 返回 `None` = 没起来 / 超时 / 被取消(三者对调用方是同一件事:这次探测没有结论)。
|
||||
///
|
||||
/// 为什么不用 `Command::output()`:它是「等到底」,一个卡住的子进程会把整个扫描线程钉死,
|
||||
/// 而取消位只能在两次探测之间被看到——取消就永远兑现不了。
|
||||
#[must_use]
|
||||
pub fn run_with_timeout(
|
||||
mut command: std::process::Command,
|
||||
timeout: Duration,
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
) -> Option<std::process::Output> {
|
||||
use std::process::Stdio;
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let mut child = command.spawn().ok()?;
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Ok(None) => {
|
||||
if cancelled() || started.elapsed() > timeout {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(POLL_INTERVAL);
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
child.wait_with_output().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::run_with_timeout;
|
||||
|
||||
#[test]
|
||||
fn a_hung_probe_is_killed_after_the_timeout() {
|
||||
// 一个「永远不返回」的子进程:不杀掉它,扫描线程就永远不结束(复审 P2-5)。
|
||||
let started = std::time::Instant::now();
|
||||
let output = run_with_timeout(
|
||||
sleeping_command(30),
|
||||
std::time::Duration::from_millis(400),
|
||||
&|| false,
|
||||
);
|
||||
assert!(output.is_none(), "超时必须判成「没有结论」");
|
||||
let elapsed = started.elapsed();
|
||||
assert!(
|
||||
elapsed < std::time::Duration::from_secs(5),
|
||||
"超时之后必须 kill 掉子进程并返回,实际等了 {elapsed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cancel_interrupts_the_probe_that_is_already_running() {
|
||||
// 取消不能只在「两次探测之间」被看到:卡住的那一个也要能打断。
|
||||
let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let tripper = flag.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
tripper.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
});
|
||||
let started = std::time::Instant::now();
|
||||
let output = run_with_timeout(
|
||||
sleeping_command(30),
|
||||
std::time::Duration::from_secs(30),
|
||||
&|| flag.load(std::sync::atomic::Ordering::SeqCst),
|
||||
);
|
||||
assert!(output.is_none(), "被取消的探测没有结论");
|
||||
assert!(
|
||||
started.elapsed() < std::time::Duration::from_secs(10),
|
||||
"取消必须打断正在跑的那个探测,而不是等它自己结束"
|
||||
);
|
||||
}
|
||||
|
||||
/// 超时之后子进程必须**真的死了**:让它延时写一个标记文件,断言那个文件从未出现。
|
||||
///
|
||||
/// 为什么要有这一条(r2 复审 P3-1):上面两条只断言「返回得快」,把 `kill()+wait()`
|
||||
/// 两行删掉它们照样绿——而没杀掉的子进程会继续跑(写它自己的文件、占句柄)。
|
||||
///
|
||||
/// 变异证明:删掉超时分支里的 `child.kill()` + `child.wait()` ⇒ 子进程活到延时结束、把
|
||||
/// 标记文件写出来 ⇒ 本条红。
|
||||
#[test]
|
||||
fn a_timed_out_child_is_really_killed() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ull-proc-kill-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.subsec_nanos())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
std::fs::create_dir_all(&dir).expect("临时目录");
|
||||
let marker = dir.join("survived.txt");
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
|
||||
let output = run_with_timeout(
|
||||
marker_command(&marker, 4),
|
||||
std::time::Duration::from_millis(300),
|
||||
&|| false,
|
||||
);
|
||||
assert!(output.is_none(), "超时没有结论");
|
||||
// 给「要是没被杀」足够的时间把标记写出来(4 秒的延时 + 余量)。
|
||||
std::thread::sleep(std::time::Duration::from_secs(6));
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"子进程在超时后仍然活着并执行了尾巴(标记文件出现了):kill 没生效"
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// 延时 `seconds` 秒之后写一个标记文件的命令(两个平台各一条,语义相同)。
|
||||
fn marker_command(marker: &std::path::Path, seconds: u32) -> std::process::Command {
|
||||
let target = marker.display().to_string();
|
||||
if cfg!(windows) {
|
||||
// PowerShell 而不是 `cmd /C "…"`:Rust 会把整个参数加一层双引号,cmd 再把里面的
|
||||
// 引号吃掉,重定向目标就被截断(实测命令立刻失败 ⇒ 本用例假红)。这里路径用
|
||||
// PowerShell 的单引号,外层只剩 Rust 加的那一层。
|
||||
let mut command = std::process::Command::new("powershell");
|
||||
command.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&format!(
|
||||
"Start-Sleep -Seconds {seconds}; Set-Content -Path '{target}' -Value alive"
|
||||
),
|
||||
]);
|
||||
command
|
||||
} else {
|
||||
let mut command = std::process::Command::new("sh");
|
||||
command.args(["-c", &format!("sleep {seconds}; echo alive > '{target}'")]);
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个必然超时的命令(两个平台都有,且在「等很久」这件事上行为一致)。
|
||||
fn sleeping_command(seconds: u32) -> std::process::Command {
|
||||
if cfg!(windows) {
|
||||
let mut command = std::process::Command::new("cmd");
|
||||
command.args(["/C", &format!("ping -n {seconds} 127.0.0.1 > NUL")]);
|
||||
command
|
||||
} else {
|
||||
let mut command = std::process::Command::new("sleep");
|
||||
command.arg(seconds.to_string());
|
||||
command
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user