feat: 扩展日志类型,支持 JVM 崩溃与启动器日志
Sync source to CNB / Sync Git ref (push) Skipped

LogType 新增 JvmCrash 与 LauncherLog,使崩溃弹窗能列出
hs_err_pid*.log、launcher_log.txt、latest_stdout.log 等日志。

- 新增 log_directories_for_type 与 is_log_file_for_type,统一
  各日志类型的目录解析与文件名筛选
- 新增 isolated_minecraft_root,兼容 versions 版本隔离布局
- get_logs 追加 JvmCrash 与 LauncherLog 两类采集
- get_logs_by_filename / get_output_by_filename /
  delete_logs_by_filename 改用目录列表按存在性查找
- get_logs_from_type 遍历多目录并按文件名去重
This commit is contained in:
AxTps committed 2026-10-11 14:53:20 +08:00
1 parent 60f3fd8816
commit a8d8e1674b
1 file changed
+100 -31
+100 -31
View File
@@ -1,6 +1,6 @@
use std::fmt::Write as _;
use std::io::{BufRead, Read, SeekFrom};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use futures::TryFutureExt;
@@ -39,6 +39,54 @@ pub struct Logs {
pub enum LogType {
InfoLog,
CrashReport,
JvmCrash,
LauncherLog,
}
fn isolated_minecraft_root(instance_root: &Path) -> Option<PathBuf> {
let versions_dir = instance_root.parent()?;
if versions_dir
.file_name()
.is_some_and(|name| name.eq_ignore_ascii_case("versions"))
{
versions_dir.parent().map(Path::to_path_buf)
} else {
None
}
}
fn log_directories_for_type(
state: &State,
game_dir: &Path,
log_type: LogType,
) -> Vec<PathBuf> {
match log_type {
LogType::InfoLog => vec![state.directories.game_logs_dir(game_dir)],
LogType::CrashReport => {
vec![state.directories.game_crash_reports_dir(game_dir)]
}
LogType::JvmCrash | LogType::LauncherLog => {
let mut directories = vec![game_dir.to_path_buf()];
if let Some(root) = isolated_minecraft_root(game_dir) {
directories.push(root);
}
directories
}
}
}
fn is_log_file_for_type(log_type: LogType, file_name: &str) -> bool {
match log_type {
LogType::JvmCrash => {
let lower = file_name.to_ascii_lowercase();
lower.starts_with("hs_err") && lower.ends_with(".log")
}
LogType::LauncherLog => matches!(
file_name.to_ascii_lowercase().as_str(),
"launcher_log.txt" | "latest_stdout.log"
),
_ => true,
}
}
const LOG_COMPACTION_THRESHOLD: usize = 20;
@@ -448,14 +496,11 @@ pub async fn get_logs_from_type(
.directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref());
let logs_folder = match log_type {
LogType::InfoLog => state.directories.game_logs_dir(&game_dir),
LogType::CrashReport => {
state.directories.game_crash_reports_dir(&game_dir)
let mut seen_files = std::collections::HashSet::new();
for logs_folder in log_directories_for_type(&state, &game_dir, log_type) {
if !logs_folder.exists() {
continue;
}
};
if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
@@ -471,6 +516,12 @@ pub async fn get_logs_from_type(
}
if let Some(file_name) = path.file_name() {
let file_name = file_name.to_string_lossy().to_string();
if !is_log_file_for_type(log_type, &file_name) {
continue;
}
if !seen_files.insert(file_name.clone()) {
continue;
}
logs.push(
Logs::build(
log_type,
@@ -507,6 +558,20 @@ pub async fn get_logs(
&mut logs,
)
.await?;
get_logs_from_type(
instance_id,
LogType::JvmCrash,
clear_contents,
&mut logs,
)
.await?;
get_logs_from_type(
instance_id,
LogType::LauncherLog,
clear_contents,
&mut logs,
)
.await?;
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
logs.sort_by(|a, b| b.age.cmp(&a.age).then(b.filename.cmp(&a.filename)));
@@ -526,13 +591,15 @@ pub async fn get_logs_by_filename(
.directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref());
let path = match log_type {
LogType::InfoLog => state.directories.game_logs_dir(&game_dir),
LogType::CrashReport => {
state.directories.game_crash_reports_dir(&game_dir)
}
}
.join(&filename);
let path = log_directories_for_type(&state, &game_dir, log_type)
.into_iter()
.map(|directory| directory.join(&filename))
.find(|path| path.is_file())
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Log file not found: {filename}"
))
})?;
let metadata = std::fs::metadata(&path)?;
let age = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
@@ -546,14 +613,15 @@ async fn get_output_by_filename_from_path(
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let logs_folder = match log_type {
LogType::InfoLog => state.directories.game_logs_dir(game_dir),
LogType::CrashReport => {
state.directories.game_crash_reports_dir(game_dir)
}
};
let path = logs_folder.join(file_name);
let path = log_directories_for_type(state, game_dir, log_type)
.into_iter()
.map(|directory| directory.join(file_name))
.find(|path| path.is_file())
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Log file not found: {file_name}"
))
})?;
let credentials = Credentials::get_all(&state.pool)
.await?
@@ -650,14 +718,15 @@ pub async fn delete_logs_by_filename(
.directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref());
let logs_folder = match log_type {
LogType::InfoLog => state.directories.game_logs_dir(&game_dir),
LogType::CrashReport => {
state.directories.game_crash_reports_dir(&game_dir)
}
};
let path = logs_folder.join(filename);
let path = log_directories_for_type(&state, &game_dir, log_type)
.into_iter()
.map(|directory| directory.join(filename))
.find(|path| path.is_file())
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Log file not found: {filename}"
))
})?;
io::remove_file(&path).await?;
Ok(())
}