feat(m187): 启动失败弹窗 + 诊断包导出,并接通 DialogQueue
用户实测「MC 启动失败找不到崩溃日志」的直接修法:失败时弹一个框,把退出码、 一句人话原因、证据形态、四条真实路径与三个按钮摆在同一个地方。 - app-iced:最小一层模态(遮罩 + 卡片 + 主/次按钮 + Esc/点遮罩关闭), 消费 presenter 的 DialogQueue(此前全仓零引用,所有弹窗都画不出来); 队列管弹窗,CrashCard 管这次失败的事实,按 DialogId 绑定。 - 弹窗区分两种失败:游戏自己写了 crash-reports(贴 Description + 栈片段)/ 游戏没来得及写(明说「这次没有生成崩溃报告」并贴出捕获的最后 30 行)/ 什么都没抓到(给最可能原因分类)。绝不对第二种说「找不到日志」。 - 导出诊断包:core 侧新 crash_bundle 写 zip(清单 + 启动器日志 + 会话 JSONL + crash-reports/* + hs_err + latest.log + 版本 JSON),落盘即脱敏(家目录 / token / 会话 id),单文件 4 MiB、总量 48 MiB 上限并在清单记截断。 - 启动器自身日志落盘(core::launch::launcher_log → <.ULL>/logs/launcher.log, 登记目录前先缓冲),补上「双击启动时诊断行直接蒸发」的缺口。 - i18n:25 条中英文案进 catalog/logs.rs + 金表。 - 取证:ULL_DIALOG_FIXTURE=crash-report|no-logs|export 在临时目录造真文件出图, wgpu 与 tiny-skia 各一套,见 DevDocs/artifacts/m187/。 未复现 M190 的「owned 文字在 wgpu 下丢字」:同一串中文两条文字路径、两种后端逐字相同 (ab-wgpu.png / ab-tinyskia.png),因此本轮未改任何渲染路径。
This commit is contained in:
1 parent
29da7e2e47
commit
414f6ff886
23 files changed
+2818
-10
No files matched your search
@@ -17,6 +17,8 @@ use unlucky_core::game::{self, VersionJson};
|
||||
use unlucky_core::launch::adapter::{
|
||||
self, AdapterOptions, AdapterWarning, LibraryInput, SnapshotInput,
|
||||
};
|
||||
use unlucky_core::launch::crash_report::{self, CrashReport, ReportContext};
|
||||
use unlucky_core::launch::launcher_log;
|
||||
use unlucky_core::launch::request::{LaunchRequest, MemoryOptions};
|
||||
use unlucky_core::launch::time::{format_rfc3339_millis, now};
|
||||
use unlucky_core::launch::tree_kill;
|
||||
@@ -216,6 +218,19 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
let id = target.id.clone();
|
||||
let tx_progress = tx.clone();
|
||||
super::job::spawn("launch", move || {
|
||||
// M187:启动器自己的运行日志从这里开始有落点(早于这一拍的行由 core 缓冲接住)。
|
||||
let logs_dir = env.layout.logs();
|
||||
launcher_log::set_dir(&logs_dir);
|
||||
let started_at = std::time::SystemTime::now();
|
||||
let mut report = ReportContext::new(
|
||||
&target.id,
|
||||
&target.name,
|
||||
&target.version_id,
|
||||
&env.data.game_home,
|
||||
&logs_dir,
|
||||
started_at,
|
||||
unlucky_core::platform::os_info::current().summary_line(),
|
||||
);
|
||||
let _ = tx_progress.send(HostTask::Runtime {
|
||||
id: id.clone(),
|
||||
runtime: InstanceRuntime::Busy {
|
||||
@@ -226,6 +241,9 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
let (request, warnings) = match build_request(&env, &target) {
|
||||
Ok(ok) => ok,
|
||||
Err(failure) => {
|
||||
let line = format!("instance={} 装配失败:{}", target.id, failure.detail);
|
||||
launcher_log::record("launch", &line);
|
||||
publish_before_start(&report, failure.stage.slug(), &failure.detail);
|
||||
let _ = tx.send(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id,
|
||||
session: String::new(),
|
||||
@@ -237,6 +255,13 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
report.run_dir = request.dirs.game_dir.clone();
|
||||
report.version_json = Some(env.game.version_json(&target.version_id));
|
||||
report.java = format!(
|
||||
"{} (major {})",
|
||||
request.java.binary.display(),
|
||||
request.java.major
|
||||
);
|
||||
for warning in &warnings {
|
||||
let _ = tx.send(HostTask::Notice(Box::new(crate::dto::NoticeView::new(
|
||||
&format!("launch-warn:{}", target.id),
|
||||
@@ -245,21 +270,33 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
warning,
|
||||
))));
|
||||
}
|
||||
// 命令行(脱敏版)进启动器日志:事故之后「到底用什么参数起的」是第一个要看的。
|
||||
let line = format!(
|
||||
"instance={} game_dir={} java={} memory={}MB",
|
||||
target.id,
|
||||
report.run_dir.display(),
|
||||
report.java,
|
||||
request.memory.max_mb.unwrap_or(0)
|
||||
);
|
||||
launcher_log::record("launch", &line);
|
||||
let config = LauncherConfig::new(env.sessions_root())
|
||||
.with_launcher(LAUNCHER_NAME, env!("CARGO_PKG_VERSION"));
|
||||
let session = match Launcher::new(config).spawn(&request) {
|
||||
Ok(session) => session,
|
||||
Err(error) => {
|
||||
let failure = LaunchFailureView::new(LaunchFailureStage::Spawn, error.to_string());
|
||||
launcher_log::record(
|
||||
"launch",
|
||||
&format!("instance={} spawn 失败:{error}", target.id),
|
||||
);
|
||||
publish_before_start(&report, "spawn", &failure.detail);
|
||||
let _ = tx.send(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id,
|
||||
session: String::new(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
exit_code: None,
|
||||
uptime_ms: 0,
|
||||
failure: Some(LaunchFailureView::new(
|
||||
LaunchFailureStage::Spawn,
|
||||
error.to_string(),
|
||||
)),
|
||||
failure: Some(failure),
|
||||
})));
|
||||
return;
|
||||
}
|
||||
@@ -278,6 +315,22 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
match session.finish() {
|
||||
Ok(handle) => {
|
||||
let outcome = super::identity::outcome_of(&handle);
|
||||
let line = format!(
|
||||
"instance={} session={session_id} exit={} type={} uptime={}ms outcome={:?} artifacts={}",
|
||||
target.id,
|
||||
handle.exit.code,
|
||||
handle.exit.exit_type.as_str(),
|
||||
handle.uptime_ms,
|
||||
outcome,
|
||||
handle.artifacts.len()
|
||||
);
|
||||
launcher_log::record("launch", &line);
|
||||
report.secrets.push(session_id.clone());
|
||||
let lines: Vec<String> = super::logs::read_lines(&handle.log_path)
|
||||
.into_iter()
|
||||
.map(|line| line.text)
|
||||
.collect();
|
||||
crash_report::publish(CrashReport::collect(&handle, &report, &lines));
|
||||
let _ = tx.send(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id,
|
||||
session: session_id,
|
||||
@@ -289,22 +342,31 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
})));
|
||||
}
|
||||
Err(error) => {
|
||||
let failure = LaunchFailureView::new(LaunchFailureStage::Spawn, error.to_string());
|
||||
let line = format!(
|
||||
"instance={} session={session_id} 收尾失败:{error}",
|
||||
target.id
|
||||
);
|
||||
launcher_log::record("launch", &line);
|
||||
publish_before_start(&report, "spawn", &failure.detail);
|
||||
let _ = tx.send(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id,
|
||||
session: session_id,
|
||||
outcome: LogOutcome::SilentFail,
|
||||
exit_code: None,
|
||||
uptime_ms: 0,
|
||||
failure: Some(LaunchFailureView::new(
|
||||
LaunchFailureStage::Spawn,
|
||||
error.to_string(),
|
||||
)),
|
||||
failure: Some(failure),
|
||||
})));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 登记一次「进程没起来」的现场(四个失败出口共用这一处,避免各写一遍 exit_type)。
|
||||
fn publish_before_start(report: &ReportContext, stage: &str, detail: &str) {
|
||||
crash_report::publish(CrashReport::before_start(report, stage, detail, stage));
|
||||
}
|
||||
|
||||
/// 停止一次启动(按 pid 回收进程树;`finish()` 会在后台线程里照常收尾)。
|
||||
pub fn stop(pid: u32) -> Result<(), String> {
|
||||
tree_kill::kill_tree(pid, std::time::Duration::from_secs(3))
|
||||
|
||||
Reference in new issue
Block a user