D4:`pick` 的排序键从「来源序」改成「版本距离 → 来源序」,全不满足时选最接近要求的那个 (托管 25 与 JAVA_HOME/扫盘 17 之间选 17,而不是按 rank 选托管)。新增 `select::decide`: 不符且**没有人显式指定 Java** 时 `ull launch` 直接报 `java_mismatch` 不 spawn(`--java` / 实例 `javaPath` 是豁免,只告警)——1.12.2 配 Java 17/25 必然起不来,警告没有信息量。 `JavaMismatch` + `check` 拆到 `mismatch.rs` 并加 `advice()`(mapping.rs 408→282 行)。 D6:`LaunchSession::finish` 的等进程改成**有界**。判据在 `startup_watch.rs`(纯逻辑): ①窗口从未出现且**连续 N 秒没有任何日志**(还在打日志就说明还在加载,慢加载不被判死); ②日志出现关闭标记后 N 秒仍未退出(实机「关窗后 java.exe 仍存活」的形状)。 到点只**停止等待、绝不杀进程**:写 `session_end(still_running)` + 索引 + `launcher.log`, 返回 `LaunchError::Watchdog`,CLI 翻成 `launch_timeout`。两个时长可配 (默认 300s/60s;`ULL_LAUNCH_STARTUP_TIMEOUT_SECS` / `ULL_LAUNCH_SHUTDOWN_GRACE_SECS`, 生效值每次启动写进 launcher.log 与人读输出)。 D10:把「被终止」与「应用错误」分开。①我们自己杀的:`tree_kill` **送达即登记** `kill_registry`(pid 登记簿,取走语义、上限 64),覆盖 GUI 从别的线程 `kill_tree` 的路径, `finish` 据此判 `killed`;②外部强杀:Windows 上「退出码 1 + 零错误输出 + 无崩溃/JVM 关键字」 判 `killed` 并标 `terminated_externally`(任务管理器与 `taskkill /F` 都是 `TerminateProcess(h, 1)`,应用真崩给的是 NTSTATUS 异常码)。 顺带:`SessionHandle` 拆 `session_handle.rs`(launch_session.rs 422→343 行)、 `NativesReport::to_json` 落 core(cli/launch.rs 腾出空间,仍 399 行)。 用例:select 距离/取舍、startup_watch 状态机、watchdog_cases(4 条真进程)、 exit_cases(外部终止与「我们杀的」区分)、pipeline 跨线程 kill。
293 lines
11 KiB
Rust
293 lines
11 KiB
Rust
//! 「启动选哪个 Java」的**纯**优先链(D13)。
|
||
//!
|
||
//! 优先级:实例覆盖(`--java` / 档案 `javaPath`)> 托管目录里**满足要求**的 JDK >
|
||
//! `JAVA_HOME` > `PATH`。实例覆盖由调用方单独判(用户明确指定了就不参与打分),
|
||
//! 这里只处理「一堆已探测的候选 + 一个要求」的挑选。
|
||
//!
|
||
//! 挑选规则(比「机械按来源排序」更贴近用户预期):
|
||
//!
|
||
//! 1. **满足要求的候选优先**——1.12.2 只能用 Java 8,托管的 21 再新也不能选;
|
||
//! 2. 全都不满足时按**版本距离**([`distance`])取最接近的那个,同距离再按来源:
|
||
//! 托管 > `JAVA_HOME` > `PATH`(D13:托管目录是启动器的地盘,用户 PATH 上的 java
|
||
//! 可能是另一个 JDK 的残留)。距离优先而非来源优先是 D4:托管里只有 25、`JAVA_HOME`
|
||
//! 是 21 时,1.12.2 该被推上更接近的 21,而不是机械地选「来源排名更高」的托管 25;
|
||
//! 3. 挑完还要决定**要不要拦**([`decide`]):不符且用户没显式指定 Java 就不启动。
|
||
//!
|
||
//! 抽成纯函数是为了可测:测试注入「托管 21 + PATH 8」这类组合,不必真装 JDK。
|
||
|
||
use super::mapping::{check, JavaMismatch, JavaRequirement};
|
||
use super::probe::{JavaRuntime, DEFAULT_MAJOR};
|
||
|
||
/// 一次启动的 Java 选择结果。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct JavaResolution {
|
||
/// 选中的运行时(一个候选都没有时 `None`)。
|
||
pub runtime: Option<JavaRuntime>,
|
||
/// 目标版本对 Java 的要求。
|
||
pub required: JavaRequirement,
|
||
/// 不符说明(`None` = 满足要求,或压根没探测到 Java)。
|
||
pub mismatch: Option<JavaMismatch>,
|
||
}
|
||
|
||
impl JavaResolution {
|
||
/// 生效的主版本号(没有运行时按缺省值)。
|
||
#[must_use]
|
||
pub fn major(&self) -> u32 {
|
||
self.runtime
|
||
.as_ref()
|
||
.map_or(DEFAULT_MAJOR, JavaRuntime::major_or_default)
|
||
}
|
||
|
||
/// 是否满足要求(没有运行时视为「不知道」,返回 `true`)。
|
||
#[must_use]
|
||
pub fn is_satisfied(&self) -> bool {
|
||
self.mismatch.is_none()
|
||
}
|
||
}
|
||
|
||
/// 候选主版本号与要求区间的**距离**(`0` = 满足要求)。
|
||
///
|
||
/// D4:主版本号是离散档位,用户装的 `17` 与 `25` 对「只能用 8」的 1.12.2 都是错的,
|
||
/// 但 `17` 只差 9 档、`25` 差 17 档——真机上「离得近」的那个起得来的概率明显更高
|
||
/// (老版本对新 class 文件的容忍度随版本差递减)。
|
||
#[must_use]
|
||
pub fn distance(major: u32, required: &JavaRequirement) -> u32 {
|
||
if major < required.minimum {
|
||
return required.minimum - major;
|
||
}
|
||
match required.maximum {
|
||
Some(maximum) if major > maximum => major - maximum,
|
||
_ => 0,
|
||
}
|
||
}
|
||
|
||
/// 从候选里挑一个:先按「离要求多远」,同距离再按来源优先序。
|
||
///
|
||
/// 探不到主版本号的候选排最后(不知道就不能算“近”)。
|
||
#[must_use]
|
||
pub fn pick<'a>(
|
||
candidates: &'a [JavaRuntime],
|
||
required: &JavaRequirement,
|
||
) -> Option<&'a JavaRuntime> {
|
||
candidates.iter().min_by_key(|runtime| {
|
||
let distance = runtime
|
||
.major
|
||
.map_or(u32::MAX, |major| distance(major, required));
|
||
// 先排「距离远」,再排来源优先序(托管 > JAVA_HOME > PATH)。
|
||
(distance, runtime.origin.rank())
|
||
})
|
||
}
|
||
|
||
/// 启动前的取舍(D4)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum LaunchVerdict {
|
||
/// 照常启动(满足要求,或用户显式指定了 Java——那是他的明确选择)。
|
||
Proceed,
|
||
/// 拦下:不满足要求且没人明确指定,起不来是必然的,那句警告没有信息量。
|
||
Block(JavaMismatch),
|
||
}
|
||
|
||
impl LaunchVerdict {
|
||
/// 需要拦下时返回不符说明。
|
||
#[must_use]
|
||
pub fn block(self) -> Option<JavaMismatch> {
|
||
match self {
|
||
Self::Proceed => None,
|
||
Self::Block(mismatch) => Some(mismatch),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 不满足要求时是「只告警」还是「拦住」(D4 的取舍,理由见 [`LaunchVerdict`])。
|
||
///
|
||
/// 判据只有两条:不符的方向不重要(两个方向都必然起不来),**是不是用户自己选的**才重要。
|
||
#[must_use]
|
||
pub fn decide(mismatch: Option<JavaMismatch>, explicitly_chosen: bool) -> LaunchVerdict {
|
||
match mismatch {
|
||
Some(mismatch) if !explicitly_chosen => LaunchVerdict::Block(mismatch),
|
||
_ => LaunchVerdict::Proceed,
|
||
}
|
||
}
|
||
|
||
/// 挑 + 判不符。
|
||
#[must_use]
|
||
pub fn resolve(candidates: &[JavaRuntime], required: &JavaRequirement) -> JavaResolution {
|
||
let runtime = pick(candidates, required).cloned();
|
||
let mismatch = runtime
|
||
.as_ref()
|
||
.and_then(|runtime| runtime.major)
|
||
.and_then(|major| check(major, required));
|
||
JavaResolution {
|
||
runtime,
|
||
required: required.clone(),
|
||
mismatch,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::{decide, distance, resolve, LaunchVerdict};
|
||
use crate::game::java::mapping::{JavaMismatch, JavaRequirement, RequirementSource};
|
||
use crate::game::java::probe::{JavaOrigin, JavaRuntime, DEFAULT_MAJOR};
|
||
|
||
fn runtime(major: u32, origin: JavaOrigin) -> JavaRuntime {
|
||
JavaRuntime {
|
||
binary: std::path::PathBuf::from(format!("java{major}")),
|
||
origin,
|
||
version: Some(format!("openjdk version \"{major}.0.1\"")),
|
||
major: Some(major),
|
||
bits: 64,
|
||
vendor: None,
|
||
}
|
||
}
|
||
|
||
fn at_least(minimum: u32) -> JavaRequirement {
|
||
JavaRequirement::new(minimum, None, RequirementSource::Table)
|
||
}
|
||
|
||
#[test]
|
||
fn satisfying_managed_runtime_beats_mismatching_system_java() {
|
||
let candidates = [
|
||
runtime(8, JavaOrigin::Path),
|
||
runtime(21, JavaOrigin::Managed),
|
||
];
|
||
let picked = resolve(&candidates, &at_least(17));
|
||
assert_eq!(picked.major(), 21);
|
||
assert!(picked.is_satisfied());
|
||
}
|
||
|
||
#[test]
|
||
fn satisfying_system_java_beats_mismatching_managed() {
|
||
// 1.12.2 只能 Java 8:托管的 21 不满足,PATH 上的 8 满足 → 选 8。
|
||
let required = JavaRequirement::new(8, Some(8), RequirementSource::Table);
|
||
let candidates = [
|
||
runtime(21, JavaOrigin::Managed),
|
||
runtime(8, JavaOrigin::Path),
|
||
];
|
||
let picked = resolve(&candidates, &required);
|
||
assert_eq!(picked.major(), 8);
|
||
assert!(picked.is_satisfied());
|
||
}
|
||
|
||
#[test]
|
||
fn managed_wins_when_both_satisfy() {
|
||
let candidates = [
|
||
runtime(21, JavaOrigin::Path),
|
||
runtime(17, JavaOrigin::JavaHome),
|
||
runtime(17, JavaOrigin::Managed),
|
||
];
|
||
assert_eq!(resolve(&candidates, &at_least(17)).major(), 17);
|
||
}
|
||
|
||
#[test]
|
||
fn java_home_beats_path_when_both_satisfy() {
|
||
let candidates = [
|
||
runtime(21, JavaOrigin::Path),
|
||
runtime(21, JavaOrigin::JavaHome),
|
||
];
|
||
let picked = resolve(&candidates, &at_least(21));
|
||
assert_eq!(
|
||
picked.runtime.map(|runtime| runtime.origin),
|
||
Some(JavaOrigin::JavaHome)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn no_candidate_keeps_the_default_and_reports_no_mismatch() {
|
||
let picked = resolve(&[], &at_least(21));
|
||
assert_eq!(picked.major(), DEFAULT_MAJOR);
|
||
assert!(picked.runtime.is_none());
|
||
assert!(picked.is_satisfied());
|
||
}
|
||
|
||
#[test]
|
||
fn unsatisfiable_candidates_are_ranked_by_distance_not_by_origin() {
|
||
// 1.12.2(min=max=8):托管的 25 与 JAVA_HOME 的 17 都不满足要求。
|
||
// 按来源排会选托管 25(rank 1 < 2),按版本距离应选 17(距离 9 vs 17)。
|
||
let required = JavaRequirement::new(8, Some(8), RequirementSource::Table);
|
||
let candidates = [
|
||
runtime(25, JavaOrigin::Managed),
|
||
runtime(17, JavaOrigin::JavaHome),
|
||
];
|
||
assert_eq!(
|
||
resolve(&candidates, &required).major(),
|
||
17,
|
||
"全不满足时应按版本距离选 17,而不是按来源选托管的 25"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn too_old_candidates_are_ranked_by_distance() {
|
||
// 需要 21+:托管 8 与 JAVA_HOME 17 都太老(距离 13 vs 4)。
|
||
let candidates = [
|
||
runtime(8, JavaOrigin::Managed),
|
||
runtime(17, JavaOrigin::JavaHome),
|
||
];
|
||
assert_eq!(
|
||
resolve(&candidates, &at_least(21)).major(),
|
||
17,
|
||
"都太老时应选最接近的那个,而不是托管目录里最老的那个"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn mismatch_is_reported_when_nothing_fits() {
|
||
let candidates = [runtime(8, JavaOrigin::Path)];
|
||
let picked = resolve(&candidates, &at_least(21));
|
||
assert_eq!(picked.major(), 8);
|
||
let mismatch = picked.mismatch.expect("应报不符");
|
||
assert_eq!(mismatch.code(), "java_too_old");
|
||
}
|
||
|
||
#[test]
|
||
fn distance_is_zero_inside_the_interval_and_positive_outside() {
|
||
let legacy = JavaRequirement::new(8, Some(8), RequirementSource::Table);
|
||
assert_eq!(distance(8, &legacy), 0);
|
||
assert_eq!(distance(17, &legacy), 9);
|
||
assert_eq!(distance(25, &legacy), 17);
|
||
let modern = at_least(21);
|
||
assert_eq!(distance(21, &modern), 0);
|
||
assert_eq!(distance(25, &modern), 0);
|
||
assert_eq!(distance(17, &modern), 4);
|
||
// 区间 8–16(假想):16 与 9 都满足,只有 17 超出一档。
|
||
let band = JavaRequirement::new(8, Some(16), RequirementSource::Table);
|
||
assert_eq!(distance(16, &band), 0);
|
||
assert_eq!(distance(17, &band), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn unknown_majors_rank_last() {
|
||
let mut unknown = runtime(17, JavaOrigin::Managed);
|
||
unknown.major = None;
|
||
let candidates = [unknown, runtime(25, JavaOrigin::JavaHome)];
|
||
let picked = resolve(&candidates, &at_least(21));
|
||
assert_eq!(
|
||
picked.runtime.map(|runtime| runtime.major),
|
||
Some(Some(25)),
|
||
"探不到主版本号的候选不能因为来源排名高就胜出"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn mismatch_blocks_only_when_nobody_chose_that_java() {
|
||
let mismatch = JavaMismatch::TooNew {
|
||
maximum: 8,
|
||
actual: 17,
|
||
};
|
||
assert_eq!(
|
||
decide(Some(mismatch), false),
|
||
LaunchVerdict::Block(mismatch)
|
||
);
|
||
assert_eq!(decide(Some(mismatch), true), LaunchVerdict::Proceed);
|
||
assert_eq!(decide(None, false), LaunchVerdict::Proceed);
|
||
assert_eq!(LaunchVerdict::Proceed.block(), None);
|
||
assert_eq!(LaunchVerdict::Block(mismatch).block(), Some(mismatch));
|
||
// 太老同样拦:装错方向不会改变「起不来」这个结局。
|
||
let too_old = JavaMismatch::TooOld {
|
||
required: 21,
|
||
actual: 17,
|
||
};
|
||
assert_eq!(decide(Some(too_old), false).block(), Some(too_old));
|
||
}
|
||
}
|