feat(app): GUI 接上 Forge/NeoForge installer processors(M45)

- host/install_processors:GUI 与 CLI 同序跑 processors(计划 → 依赖库 → 选 Java → 串行 java -cp),
  进度经 HostTask::Install 回流(阶段文案 + 每步处理器名 + 文件/字节/速度/ETA),取消在阶段边界生效;
- host/install_journal:安装过程追加到 .ULL/logs/install.log,失败文案带日志路径,不再静默失败;
- host/install.rs:supported() 扩到原版 + Fabric/Quilt/Forge/NeoForge,删除 Forge/NeoForge 拒装路径
  (仅剩 OptiFine/LiteLoader 两家 core 侧无管线者),spawn_install 统一持 Job 上下文;
- 失败诊断:缺 Java / processor 非零退出(保留 processor_* 大类码与脱敏 stderr 尾部)/ 依赖库下漏
  / 元数据缺失,均为结构化文案 + Toast + 装日志;
- dto::InstallStage::Installing 文案改「生成版本」(同时覆盖写版本 JSON 与跑 installer 两段);
- screenshot:新增 --screenshot-loader,Forge/NeoForge 中间态可离屏取图;
- 测试 +14(138 全绿):supported() 语义、处理器阶段注入 fake 后端覆盖跳过/缺 Java/非零退出/
  依赖库失败/命令行构造/进度文案,另修旧用例(Forge 不再被门禁拒装)。
This commit is contained in:
AskaEth committed 2026-11-01 09:14:01 +08:00
1 parent 858c227900
commit b0f747074f
9 files changed
+1182 -79

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
//! 安装过程的可回溯日志(`.ULL/logs/install.log`,一轮一行)。
//!
//! 起因(M45):Forge/NeoForge 装失败时,GUI 只有一行 Toast,processor 的 stderr 尾部
//! 与「哪一步挂了」在面板一关就没了。这里把安装过程按行追加到 `.ULL/logs/`
//! (与 `auth.jsonl` 同一分区),失败文案里带上日志路径,用户能自己回看。
//!
//! 三条纪律(与 auth 日志同源):
//! 1. **只追加**,不轮转、不截断(体积由「一行一条、只记诊断字段」控制);
//! 2. **只记诊断字段**:阶段、processor 坐标、退出码、已脱敏的 stderr 尾部(core 侧脱敏);
//! 3. **写盘尽力而为**:开不了文件就静默跳过,绝不让日志失败连累安装。
use std::io::Write;
use std::path::{Path, PathBuf};
use unlucky_core::launch::time;
use super::env::HostEnv;
/// 日志文件名(在 `.ULL/logs/` 下)。
pub const INSTALL_LOG_FILE: &str = "install.log";
/// 一轮安装的日志句柄(`Clone` 很便宜:只有一个路径)。
#[derive(Debug, Clone)]
pub struct Journal {
path: PathBuf,
}
impl Journal {
/// 按数据根打开(`logs/` 不存在就补建;建不出来只是没有日志)。
pub fn open(env: &HostEnv) -> Self {
let dir = env.layout.logs();
let _ = std::fs::create_dir_all(&dir);
Self {
path: dir.join(INSTALL_LOG_FILE),
}
}
/// 日志路径(失败文案里给用户看)。
pub fn path(&self) -> &Path {
&self.path
}
/// 追加一行(自动补 RFC3339 时间戳;写失败静默)。
pub fn line(&self, text: &str) {
let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
else {
return;
};
let _ = writeln!(
file,
"[{}] {text}",
time::format_rfc3339_millis(time::now())
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lines_are_appended_with_a_timestamp() {
let root = std::env::temp_dir().join(format!("ull-install-journal-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let env = HostEnv::at_root(&root);
let journal = Journal::open(&env);
journal.line("第一行");
journal.line("第二行");
let text = std::fs::read_to_string(journal.path()).expect("日志存在");
assert!(text.contains("第一行") && text.contains("第二行"), "{text}");
assert_eq!(text.lines().count(), 2);
assert!(text.starts_with("[2"), "带时间戳:{text}");
assert_eq!(journal.path(), env.layout.logs().join(INSTALL_LOG_FILE));
std::fs::remove_dir_all(&root).ok();
}
}