fix(cli): 启动用的 Java 钉死 D4 校验过的那个 + M214 复审 nit1/nit2/nit3
- override_path 五来源统一钉住 D4 选中的运行时:以前只有 managed 钉路径, JAVA_HOME/PATH/system 三种会被 snapshot::prepare 的 selection() 按环境再挑一遍, D4 判过的 Java 根本不是真启动的那个。现在 `plan_java` 只做「选中 → 钉路径」, 人读行写出「版本(来源 · 具体可执行文件)」,--json 的 java 块并排给出 binary(校验过的)/ launch_binary(装配落地的)/ source,两者不一致时当场告警。 实例设置/全局设置里的 javaPath 也并入「谁指定了 Java」,不会被自动挑的结果顶掉。 - nit3:D4 的 Java 不符拦截挪到 identity::resolve 之前(微软轨解析身份会联网刷新 令牌、还会回写凭据),拦下的启动不再先触发一次刷新;不符提示补上「挑中的是哪个 java」,文案只加不减。 - nit1:error.rs / launcher.rs 的人读串里残留的 ** 标记去掉,并补两条回归断言。 - nit2:java_launch.rs 里「只提示不阻断」等旧注释对齐 D4 现状。 - launch.rs(399 行零余量):D20 的隔离档位派生与切换检测整体搬到新文件 launch_isolation.rs,纯挪函数体(逐行 diff 只多 6 处 pub(super)), 用 #[path] 挂载以免改 commands/mod.rs。 用例:钉路径纯函数单测 3 条 + 端到端 2 条(JAVA_HOME 置空证明非 managed 来源也钉住; 数据根放两个账户证明 D4 拦在身份解析之前)+ 人读串无 ** 的 2 条回归。 两条变异(①退回只钉 Managed ②退回身份解析之后)各自让新用例变红,已实测。
This commit is contained in:
1 parent
1f6e15127a
commit
75aa5ee82c
8 files changed
+577
-196
No files matched your search
@@ -0,0 +1,114 @@
|
||||
//! M220:`ull launch` 真正 spawn 的 Java 必须是 D4 校验过的那个。
|
||||
//!
|
||||
//! 钉路径的**决策**是纯函数(`commands::java_launch::plan_java`,那边有五种来源的单测);
|
||||
//! 本文件补**端到端**:装配之后命令行里那个 java 也得是同一个。
|
||||
//!
|
||||
//! 手法:把 `JAVA_HOME` 指到一个**没有 `bin/java` 的空目录**,让「装配期按环境兜底」
|
||||
//! 与「D4 选中的候选」必然不同——兜底会退回裸名字 `java[.exe]`(相对名,靠 `PATH` 找),
|
||||
//! 而 D4 选中的是探出来的绝对路径。不动 `JAVA_HOME` 的话兜底恰好也落在 `JAVA_HOME`
|
||||
//! 那一个文件上,「只钉 Managed」的老实现也能蒙对,正是复审抓到的那个缺陷。
|
||||
//!
|
||||
//! 用真子进程(`CARGO_BIN_EXE_ull` + `Command::env`)而不是 `support::run_cli`:
|
||||
//! `JAVA_HOME` 要按进程注入,改本进程的环境会与同二进制里的其它用例抢。
|
||||
|
||||
#[path = "support/mod.rs"]
|
||||
mod support;
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use support::{parse_json, write_version, TempDir};
|
||||
|
||||
/// 一个可组装的版本 id:静态表要求 **Java 21+**。
|
||||
///
|
||||
/// 选 21+ 是有意的:「托管区里只有 17(距离 4)、环境/系统上有 21/25(距离 0)」,
|
||||
/// 按 D4 的版本距离优先,选中的必然是托管的**外面**那个——正是复审抓到的
|
||||
/// 「只有 managed 会钉路径」缺陷所在的那条分支。要求 17 的话托管 17 会赢,
|
||||
/// 老实现也能蒙对,用例就白跑了。
|
||||
const VERSION: &str = "1.21";
|
||||
|
||||
/// 跑一次 `ull --data-dir <root> <tail...>`,`JAVA_HOME` 注入成 `java_home`。
|
||||
///
|
||||
/// `json` 决定加不加全局 `--json`(加了就只有 JSON envelope,人读行被收起来)。
|
||||
fn cli_with_java_home(
|
||||
dir: &TempDir,
|
||||
java_home: &Path,
|
||||
json: bool,
|
||||
tail: &[&str],
|
||||
) -> (i32, String, String) {
|
||||
let mut args = vec!["--data-dir".to_string(), dir.arg()];
|
||||
if json {
|
||||
args.push("--json".to_string());
|
||||
}
|
||||
args.extend(tail.iter().map(|arg| arg.to_string()));
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_ull"))
|
||||
.args(&args)
|
||||
.env("JAVA_HOME", java_home)
|
||||
.output()
|
||||
.expect("应能起 ull");
|
||||
(
|
||||
output.status.code().unwrap_or(-1),
|
||||
String::from_utf8_lossy(&output.stdout).into_owned(),
|
||||
String::from_utf8_lossy(&output.stderr).into_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_command_line_runs_the_java_that_d4_validated() {
|
||||
let dir = TempDir::new("java-pin");
|
||||
write_version(dir.path(), VERSION);
|
||||
// 空 JAVA_HOME:`JAVA_HOME/bin/java` 不存在 → 环境兜底这条路走不通。
|
||||
let empty_home = dir.path().join("empty-java-home");
|
||||
std::fs::create_dir_all(&empty_home).expect("建空 JAVA_HOME");
|
||||
|
||||
let (code, stdout, stderr) =
|
||||
cli_with_java_home(&dir, &empty_home, true, &["launch", VERSION, "--dry-run"]);
|
||||
assert_eq!(code, 0, "stderr: {stderr}");
|
||||
let data = parse_json(&stdout)["data"].clone();
|
||||
let java = &data["java"];
|
||||
let source = java["source"].as_str().unwrap_or("none");
|
||||
assert!(
|
||||
["PATH", "system"].contains(&source),
|
||||
"前置:`JAVA_HOME` 已置空,且这条版本要 Java 21+(托管区的 17 不满足),\
|
||||
来源只该是 PATH 或标准安装位置:{data}"
|
||||
);
|
||||
let binary = java["binary"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("本机一个 Java 都探不到,这条端到端验不到东西(判红):{data}"));
|
||||
assert!(
|
||||
Path::new(binary).is_absolute(),
|
||||
"前置:探到的 Java 必须是绝对路径(否则「钉住」与「裸名字」无从区分,判红):{data}"
|
||||
);
|
||||
|
||||
// ① 装配落地的就是校验过的那个(`--json` 里并排给的两个事实必须相等)。
|
||||
assert_eq!(
|
||||
java["launch_binary"].as_str(),
|
||||
Some(binary),
|
||||
"装配落地的 java 必须是 D4 校验过的那个:{data}"
|
||||
);
|
||||
// ② 命令行里真正要跑的也是它 —— 这一条才是「spawn 的是哪个」。
|
||||
// 注意 `program` 会过脱敏(主目录 → `<home>`),所以按「绝对路径 + 文件名」判,
|
||||
// 不按整串判;退回裸 `java[.exe]` 的老实现在这里必红。
|
||||
let program = data["program"].as_str().unwrap_or_default();
|
||||
assert!(
|
||||
program.starts_with("<home>") || Path::new(program).is_absolute(),
|
||||
"命令行里的 java 必须是钉住的绝对路径(裸 `java` = 又退回装配期重选了):{data}"
|
||||
);
|
||||
assert_eq!(
|
||||
Path::new(program).file_name(),
|
||||
Path::new(binary).file_name(),
|
||||
"命令行里的 java 与 D4 校验的不是同一个:{data}"
|
||||
);
|
||||
// ③ 人读输出也要说清「实际用哪个、来源是什么」(终端里对着日志排查时只有一个依据)。
|
||||
let (code, stdout, stderr) =
|
||||
cli_with_java_home(&dir, &empty_home, false, &["launch", VERSION, "--dry-run"]);
|
||||
assert_eq!(code, 0, "stderr: {stderr}");
|
||||
assert!(
|
||||
stdout.contains(&format!("{source} · ")),
|
||||
"人读行要写出 Java 来源:{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains(binary),
|
||||
"人读行要写出实际使用的 java 路径({binary}):{stdout}"
|
||||
);
|
||||
}
|
||||
@@ -138,6 +138,11 @@ fn launch_dry_run_reports_the_java_requirement_of_the_version() {
|
||||
["instance", "managed", "JAVA_HOME", "PATH", "none"].contains(&source),
|
||||
"意外的来源:{source}"
|
||||
);
|
||||
// M220:`binary` 是 D4 校验过的那个,`launch_binary` 是装配落地的那个——两者都得给出来。
|
||||
assert!(
|
||||
java["launch_binary"].as_str().is_some(),
|
||||
"`--json` 的 java 块要写明实际装配的 java 路径:{json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -195,3 +200,95 @@ fn legacy_version_requires_java_8_and_flags_a_newer_runtime() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 写一份「清单自己声明要 Java `required`」的版本 JSON(其余字段与 `support::write_version` 同)。
|
||||
///
|
||||
/// 本机任何一个 Java 都不满足 `required = 99` ⇒ D4 必然要拦(`java_too_old`),
|
||||
/// 与「这台机器装了哪几个 JDK」无关、也不需要联网:用例的观察点是**拦截发生在哪一步**。
|
||||
fn write_version_requiring_java(root: &std::path::Path, required: u32) {
|
||||
let dir = root.join(".minecraft").join("versions").join(VERSION);
|
||||
std::fs::create_dir_all(&dir).expect("创建版本目录");
|
||||
let version = serde_json::json!({
|
||||
"id": VERSION,
|
||||
"type": "release",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"assets": "5",
|
||||
"assetIndex": { "id": "5" },
|
||||
"javaVersion": { "majorVersion": required },
|
||||
"arguments": {
|
||||
"game": ["--username", "${auth_player_name}", "--version", "${version_name}"],
|
||||
"jvm": ["-Djava.library.path=${natives_directory}", "-cp", "${classpath}"]
|
||||
},
|
||||
"libraries": [],
|
||||
"downloads": { "client": { "size": 4, "url": "http://example.invalid/client.jar" } }
|
||||
});
|
||||
std::fs::write(
|
||||
dir.join(format!("{VERSION}.json")),
|
||||
serde_json::to_vec_pretty(&version).expect("序列化版本 JSON"),
|
||||
)
|
||||
.expect("写版本 JSON");
|
||||
std::fs::write(dir.join(format!("{VERSION}.jar")), b"junk").expect("写 client jar");
|
||||
}
|
||||
|
||||
/// M220 nit3:D4 的「Java 不符」拦截必须排在身份解析**之前**。
|
||||
///
|
||||
/// 身份解析有副作用(微软轨会联网刷新令牌、刷新成功还会回写凭据),一个注定要被拦下的
|
||||
/// 启动不该先跑那一遍。观察点:数据根里放两个离线账户 ⇒ 解析身份必然报 `account_ambiguous`,
|
||||
/// 而这次启动注定被 D4 拦下 ⇒ 报出来的必须是 `java_mismatch`。
|
||||
/// 老实现(拦在身份之后)在这里会先撞上 `account_ambiguous`,用例必红。
|
||||
///
|
||||
/// 对照组:`--dry-run` 本来就只提示不拦(D4 只管真启动),所以同一条版本在 dry-run 下
|
||||
/// 会走到身份解析那一步——这一对断言把「拦截在哪一步」钉死。
|
||||
#[test]
|
||||
fn the_java_mismatch_block_runs_before_identity_resolution() {
|
||||
let dir = TempDir::new("java-block-order");
|
||||
write_version_requiring_java(dir.path(), 99);
|
||||
for name in ["alpha", "beta"] {
|
||||
let (code, _, stderr) =
|
||||
run_cli(&["accounts", "add", "offline", name, "--data-dir", &dir.arg()]);
|
||||
assert_eq!(code, 0, "建离线账户失败:{stderr}");
|
||||
}
|
||||
|
||||
// 前置:这条版本的要求确实没人满足(否则「注定被拦」这个前提不成立,直接判红)。
|
||||
let (code, stdout, stderr) = run_cli(&[
|
||||
"--json",
|
||||
"--data-dir",
|
||||
&dir.arg(),
|
||||
"launch",
|
||||
VERSION,
|
||||
"--dry-run",
|
||||
"--account",
|
||||
"alpha",
|
||||
]);
|
||||
assert_eq!(code, 0, "stderr: {stderr}");
|
||||
let envelope = parse_json(&stdout);
|
||||
assert_eq!(
|
||||
envelope["data"]["java"]["mismatch"]["code"], "java_too_old",
|
||||
"前置:本机探测到的 Java 必须都不满足 Java 99:{envelope}"
|
||||
);
|
||||
|
||||
// 真启动(不指定账户):两个账户 ⇒ 解析身份会报 account_ambiguous,但 D4 拦得更早。
|
||||
let (code, stdout, _stderr) = run_cli(&["--json", "--data-dir", &dir.arg(), "launch", VERSION]);
|
||||
assert_eq!(code, 1);
|
||||
let envelope = parse_json(&stdout);
|
||||
assert_eq!(
|
||||
envelope["error"]["code"], "java_mismatch",
|
||||
"D4 拦截必须排在身份解析之前(否则注定被拦的启动也先刷了一遍令牌):{envelope}"
|
||||
);
|
||||
|
||||
// 对照:dry-run 不拦(D4 只管真启动)⇒ 它会走到身份解析,报的是账户歧义。
|
||||
let (code, stdout, _stderr) = run_cli(&[
|
||||
"--json",
|
||||
"--data-dir",
|
||||
&dir.arg(),
|
||||
"launch",
|
||||
VERSION,
|
||||
"--dry-run",
|
||||
]);
|
||||
assert_eq!(code, 1);
|
||||
let envelope = parse_json(&stdout);
|
||||
assert_eq!(
|
||||
envelope["error"]["code"], "account_ambiguous",
|
||||
"dry-run 不该被 D4 拦住(它只提示),应当走到身份解析:{envelope}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user