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:
1 parent
858c227900
commit
b0f747074f
9 files changed
+1182
-79
No files matched your search
+120
-44
@@ -1,12 +1,14 @@
|
||||
//! 安装:新建实例向导 / 下载页「安装」→ core 安装管线(原版 + Fabric/Quilt)→ 登记实例。
|
||||
//! 安装:新建实例向导 / 下载页「安装」→ core 安装管线 → 登记实例。
|
||||
//!
|
||||
//! 与 CLI `ull install` 同序:`RemoteSource::resolve → InstallPlan::build → execute → register`。
|
||||
//! 差异:进度经 [`super::job::HostTask::Install`] 回流(文件计数 + 字节 + 速度/ETA),
|
||||
//! Forge/NeoForge 多一段 installer processors([`super::install_processors`]):
|
||||
//! `ProcessorPlan::build → 依赖库下载 → 选 Java → 串行 java -cp`,**跑完才写版本 JSON**。
|
||||
//! 差异只在线程模型:进度经 [`super::job::HostTask::Install`] 回流(文件计数 + 字节 + 速度/ETA),
|
||||
//! 且整段跑在后台线程里(`block_on` 不占 UI 线程)。
|
||||
//!
|
||||
//! 加载器范围:原版 + Fabric/Quilt(纯库层)。Forge/NeoForge 要外调 java 跑 installer
|
||||
//! processors(M35 的执行器在 CLI 侧),GUI 这一轮**显式拒装并说清去哪装**,
|
||||
//! 而不是装出一个启动必崩的骨架。
|
||||
//! 加载器范围:原版 + Fabric/Quilt/Forge/NeoForge(四家,与 CLI 对齐)。OptiFine/LiteLoader
|
||||
//! 两家 core 侧都还没有安装管线,仍在 GUI 里显式拒装并说清去哪装。
|
||||
//! 失败一律落 `.ULL/logs/install.log`([`super::install_journal`]),文案里带日志路径。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -27,6 +29,8 @@ use crate::dto::{CreateRequest, InstallStage, LoaderKind};
|
||||
use crate::toast::{Toast, ToastKind};
|
||||
|
||||
use super::env::HostEnv;
|
||||
use super::install_journal::Journal;
|
||||
use super::install_processors::{self, StageInput};
|
||||
use super::job::{spawn, HostTask, JobSender};
|
||||
use super::meter::{sink_for, Meter};
|
||||
|
||||
@@ -36,6 +40,26 @@ pub const CONCURRENCY: usize = 16;
|
||||
/// 用户取消的稳定文案(阶段边界生效)。
|
||||
pub const CANCELLED: &str = "已取消(阶段边界停下;已下载的文件保留,重装会续用)";
|
||||
|
||||
/// 一次安装任务的运行上下文(后台线程内一路传下去,免得每个函数排七八个参数)。
|
||||
struct Job<'a> {
|
||||
env: &'a HostEnv,
|
||||
tx: &'a JobSender,
|
||||
meter: &'a Arc<std::sync::Mutex<Meter>>,
|
||||
cancel: &'a Arc<AtomicBool>,
|
||||
journal: &'a Journal,
|
||||
}
|
||||
|
||||
impl Job<'_> {
|
||||
/// processor 段的进度出口(同一套 channel/meter/journal)。
|
||||
fn reporter(&self) -> install_processors::Reporter<'_> {
|
||||
install_processors::Reporter {
|
||||
tx: self.tx,
|
||||
meter: self.meter,
|
||||
journal: self.journal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载器徽标(`原版` / `Fabric 0.16.5`)。
|
||||
pub fn loader_label(kind: LoaderKind, version: &str) -> String {
|
||||
if kind == LoaderKind::Vanilla || version.trim().is_empty() {
|
||||
@@ -45,20 +69,26 @@ pub fn loader_label(kind: LoaderKind, version: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// GUI 侧支持的加载器(Forge/NeoForge 需要 installer processors)。
|
||||
/// GUI 侧支持的加载器(五选:原版 + 四家加载器,与 CLI 的 `--loader` 词表一致)。
|
||||
///
|
||||
/// 写成穷举 `matches!` 而不是 `true`:将来 core 加了第六种加载器,这里默认拒绝,
|
||||
/// 由 [`unsupported_reason`] 给出明确说法,而不是让它走进不属于它的安装分支。
|
||||
pub fn supported(kind: LoaderKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
LoaderKind::Vanilla | LoaderKind::Fabric | LoaderKind::Quilt
|
||||
LoaderKind::Vanilla
|
||||
| LoaderKind::Fabric
|
||||
| LoaderKind::Quilt
|
||||
| LoaderKind::Forge
|
||||
| LoaderKind::NeoForge
|
||||
)
|
||||
}
|
||||
|
||||
/// 不支持的加载器 → 给用户的可执行建议。
|
||||
pub fn unsupported_reason(kind: LoaderKind) -> String {
|
||||
format!(
|
||||
"{} 需要 installer processors(外调 java 改 jar),GUI 这一轮还没接入;请先在终端跑 `ull install <版本> --loader {}`,装完回实例页刷新即可启动。",
|
||||
kind.label(),
|
||||
kind.as_str()
|
||||
"{} 还没有接入安装管线(目前支持原版 + Fabric/Quilt/Forge/NeoForge);请先在实例页导入已有版本,或换成受支持的加载器。",
|
||||
kind.label()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,13 +96,28 @@ pub fn unsupported_reason(kind: LoaderKind) -> String {
|
||||
pub fn spawn_install(env: HostEnv, tx: JobSender, request: CreateRequest, cancel: Arc<AtomicBool>) {
|
||||
spawn("install", move || {
|
||||
let meter = Arc::new(std::sync::Mutex::new(Meter::new(&request)));
|
||||
let result = run(&env, &tx, &request, &meter, &cancel);
|
||||
let journal = Journal::open(&env);
|
||||
journal.line(&format!(
|
||||
"—— 开始安装 {}({} {})——",
|
||||
request.instance_name(),
|
||||
loader_label(request.loader, ""),
|
||||
request.version
|
||||
));
|
||||
let job = Job {
|
||||
env: &env,
|
||||
tx: &tx,
|
||||
meter: &meter,
|
||||
cancel: &cancel,
|
||||
journal: &journal,
|
||||
};
|
||||
let result = run(&job, &request);
|
||||
let mut progress = meter
|
||||
.lock()
|
||||
.map(|guard| guard.progress.clone())
|
||||
.unwrap_or_else(|_| Meter::new(&request).progress);
|
||||
match result {
|
||||
Ok(name) => {
|
||||
journal.line(&format!("完成:实例 {name} 已登记"));
|
||||
progress.stage = InstallStage::Done;
|
||||
progress.files_done = progress.files_total;
|
||||
progress.message = format!("已安装 {name}({})", progress.loader);
|
||||
@@ -88,15 +133,24 @@ pub fn spawn_install(env: HostEnv, tx: JobSender, request: CreateRequest, cancel
|
||||
})));
|
||||
}
|
||||
Err(reason) => {
|
||||
progress.stage = if reason == CANCELLED {
|
||||
let cancelled = reason == CANCELLED;
|
||||
progress.stage = if cancelled {
|
||||
InstallStage::Cancelled
|
||||
} else {
|
||||
InstallStage::Failed
|
||||
};
|
||||
progress.message.clone_from(&reason);
|
||||
// 失败文案带上日志路径:面板只有一行、Toast 4 秒就消失,
|
||||
// 装日志是用户事后能自己回看的唯一渠道(进度板之外)。
|
||||
let text = if cancelled {
|
||||
reason
|
||||
} else {
|
||||
journal.line(&format!("安装未完成:{reason}"));
|
||||
format!("{reason}(日志:{})", journal.path().display())
|
||||
};
|
||||
progress.message.clone_from(&text);
|
||||
let _ = tx.send(HostTask::InstallDone {
|
||||
progress: Box::new(progress),
|
||||
result: Err(reason),
|
||||
result: Err(text),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -104,13 +158,7 @@ pub fn spawn_install(env: HostEnv, tx: JobSender, request: CreateRequest, cancel
|
||||
}
|
||||
|
||||
/// 真安装(同步跑完,调用方在后台线程里)。
|
||||
fn run(
|
||||
env: &HostEnv,
|
||||
tx: &JobSender,
|
||||
request: &CreateRequest,
|
||||
meter: &Arc<std::sync::Mutex<Meter>>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
) -> Result<String, String> {
|
||||
fn run(job: &Job<'_>, request: &CreateRequest) -> Result<String, String> {
|
||||
if request.version.trim().is_empty() {
|
||||
return Err("没有选择版本".to_string());
|
||||
}
|
||||
@@ -118,26 +166,31 @@ fn run(
|
||||
return Err(unsupported_reason(request.loader));
|
||||
}
|
||||
let name = request.instance_name();
|
||||
let env = job.env;
|
||||
let engine = super::engine::engine(env);
|
||||
{
|
||||
let mut guard = meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
let mut guard = job.meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
guard.stage(InstallStage::Planning, "正在解析版本清单…");
|
||||
let _ = tx.send(HostTask::Install(Box::new(guard.progress.clone())));
|
||||
let _ = job
|
||||
.tx
|
||||
.send(HostTask::Install(Box::new(guard.progress.clone())));
|
||||
}
|
||||
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
if job.cancel.load(Ordering::SeqCst) {
|
||||
return Err(CANCELLED.to_string());
|
||||
}
|
||||
let version_id = if request.loader == LoaderKind::Vanilla {
|
||||
install_vanilla(env, &engine, request, tx, meter, cancel)?
|
||||
install_vanilla(job, &engine, request)?
|
||||
} else {
|
||||
install_loader(env, &engine, request, tx, meter, cancel)?
|
||||
install_loader(job, &engine, request)?
|
||||
};
|
||||
|
||||
{
|
||||
let mut guard = meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
let mut guard = job.meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
guard.stage(InstallStage::Registering, "正在登记实例…");
|
||||
let _ = tx.send(HostTask::Install(Box::new(guard.progress.clone())));
|
||||
let _ = job
|
||||
.tx
|
||||
.send(HostTask::Install(Box::new(guard.progress.clone())));
|
||||
}
|
||||
let created_at =
|
||||
unlucky_core::launch::time::format_rfc3339_millis(unlucky_core::launch::time::now());
|
||||
@@ -153,17 +206,15 @@ fn run(
|
||||
|
||||
/// 原版安装(`resolve → plan → [prefetch 资源索引] → execute`)。
|
||||
fn install_vanilla(
|
||||
env: &HostEnv,
|
||||
job: &Job<'_>,
|
||||
engine: &DownloadEngine,
|
||||
request: &CreateRequest,
|
||||
tx: &JobSender,
|
||||
meter: &Arc<std::sync::Mutex<Meter>>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
) -> Result<String, String> {
|
||||
let env = job.env;
|
||||
let version = request.version.trim();
|
||||
let meta_cache = env.layout.meta_cache();
|
||||
let now = now_ms();
|
||||
let sink = sink_for(tx.clone(), Arc::clone(meter));
|
||||
let sink = sink_for(job.tx.clone(), Arc::clone(job.meter));
|
||||
block_on(async {
|
||||
let source = RemoteSource::new(engine, &env.game, &meta_cache, now);
|
||||
let resolved = source.resolve(version).await.map_err(install_error)?;
|
||||
@@ -197,7 +248,7 @@ fn install_vanilla(
|
||||
}
|
||||
let (bytes_total, _) = plan.total_bytes();
|
||||
{
|
||||
let mut guard = meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
let mut guard = job.meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
guard.progress.files_total = u64::try_from(plan.file_count()).unwrap_or(u64::MAX);
|
||||
guard.progress.bytes_total = bytes_total;
|
||||
guard.stage(
|
||||
@@ -205,7 +256,7 @@ fn install_vanilla(
|
||||
&format!("开始下载 {} 个文件…", plan.file_count()),
|
||||
);
|
||||
}
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
if job.cancel.load(Ordering::SeqCst) {
|
||||
return Err(CANCELLED.to_string());
|
||||
}
|
||||
let report = execute(engine, &plan, CONCURRENCY, Some(sink))
|
||||
@@ -216,15 +267,13 @@ fn install_vanilla(
|
||||
})
|
||||
}
|
||||
|
||||
/// 加载器安装(原版前置 → 加载器库 → 版本 JSON 落盘)。
|
||||
/// 加载器安装(原版前置 → installer processors → 加载器库 → 版本 JSON 落盘)。
|
||||
fn install_loader(
|
||||
env: &HostEnv,
|
||||
job: &Job<'_>,
|
||||
engine: &DownloadEngine,
|
||||
request: &CreateRequest,
|
||||
tx: &JobSender,
|
||||
meter: &Arc<std::sync::Mutex<Meter>>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
) -> Result<String, String> {
|
||||
let env = job.env;
|
||||
let version = request.version.trim();
|
||||
let kind = CoreLoaderKind::parse(request.loader.as_str())
|
||||
.ok_or_else(|| format!("未知加载器 `{}`", request.loader.as_str()))?;
|
||||
@@ -237,6 +286,12 @@ fn install_loader(
|
||||
.await
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
job.journal.line(&format!(
|
||||
"加载器解析:{} {} → {}",
|
||||
release.kind.label(),
|
||||
release.loader_version,
|
||||
release.version_id
|
||||
));
|
||||
|
||||
let plan = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &env.game,
|
||||
@@ -247,10 +302,31 @@ fn install_loader(
|
||||
if plan.vanilla_install_needed {
|
||||
let mut vanilla = request.clone();
|
||||
vanilla.loader = LoaderKind::Vanilla;
|
||||
install_vanilla(env, engine, &vanilla, tx, meter, cancel)?;
|
||||
install_vanilla(job, engine, &vanilla)?;
|
||||
}
|
||||
// Forge/NeoForge:installer processors 必须先跑完,`execute` 才允许写版本 JSON。
|
||||
if release.requires_installer() {
|
||||
let input = StageInput {
|
||||
kind: release.kind,
|
||||
mc_version: &release.mc_version,
|
||||
version_id: &release.version_id,
|
||||
profile: release.install_profile.as_ref(),
|
||||
installer: release.installer.as_deref(),
|
||||
installed: env.game.is_version_installed(&release.version_id),
|
||||
};
|
||||
// `None` = 这一版不需要跑处理器(版本 JSON 已在盘上 / profile 没有 client 侧步骤),
|
||||
// 进度与日志都由处理器段自己写全,这里只消费返回值。
|
||||
let _ = install_processors::stage(
|
||||
env,
|
||||
engine,
|
||||
&input,
|
||||
&install_processors::CORE,
|
||||
&job.reporter(),
|
||||
job.cancel,
|
||||
)?;
|
||||
}
|
||||
{
|
||||
let mut guard = meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
let mut guard = job.meter.lock().map_err(|_| "进度锁失效".to_string())?;
|
||||
guard.progress.files_total += u64::try_from(plan.files().len()).unwrap_or(u64::MAX);
|
||||
let (bytes, _) = plan.install.total_bytes();
|
||||
guard.progress.bytes_total = guard.progress.bytes_total.max(bytes);
|
||||
@@ -259,10 +335,10 @@ fn install_loader(
|
||||
&format!("开始下载加载器库({} 个文件)…", plan.files().len()),
|
||||
);
|
||||
}
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
if job.cancel.load(Ordering::SeqCst) {
|
||||
return Err(CANCELLED.to_string());
|
||||
}
|
||||
let sink = sink_for(tx.clone(), Arc::clone(meter));
|
||||
let sink = sink_for(job.tx.clone(), Arc::clone(job.meter));
|
||||
let report = block_on(async {
|
||||
execute(engine, &plan.install, CONCURRENCY, Some(sink))
|
||||
.await
|
||||
|
||||
Reference in new issue
Block a user