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::fmt::Write as _;
use std::io::{BufRead, Read, SeekFrom}; use std::io::{BufRead, Read, SeekFrom};
use std::path::Path; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
use futures::TryFutureExt; use futures::TryFutureExt;
@@ -39,6 +39,54 @@ pub struct Logs {
pub enum LogType { pub enum LogType {
InfoLog, InfoLog,
CrashReport, 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; const LOG_COMPACTION_THRESHOLD: usize = 20;
@@ -448,14 +496,11 @@ pub async fn get_logs_from_type(
.directories .directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref()); .resolve_game_dir(&instance_path, game_dir_override.as_deref());
let logs_folder = match log_type { let mut seen_files = std::collections::HashSet::new();
LogType::InfoLog => state.directories.game_logs_dir(&game_dir), for logs_folder in log_directories_for_type(&state, &game_dir, log_type) {
LogType::CrashReport => { if !logs_folder.exists() {
state.directories.game_crash_reports_dir(&game_dir) continue;
} }
};
if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder) for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &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() { if let Some(file_name) = path.file_name() {
let file_name = file_name.to_string_lossy().to_string(); 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.push(
Logs::build( Logs::build(
log_type, log_type,
@@ -507,6 +558,20 @@ pub async fn get_logs(
&mut logs, &mut logs,
) )
.await?; .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>>>()?; 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))); 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 .directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref()); .resolve_game_dir(&instance_path, game_dir_override.as_deref());
let path = match log_type { let path = log_directories_for_type(&state, &game_dir, log_type)
LogType::InfoLog => state.directories.game_logs_dir(&game_dir), .into_iter()
LogType::CrashReport => { .map(|directory| directory.join(&filename))
state.directories.game_crash_reports_dir(&game_dir) .find(|path| path.is_file())
} .ok_or_else(|| {
} crate::ErrorKind::OtherError(format!(
.join(&filename); "Log file not found: {filename}"
))
})?;
let metadata = std::fs::metadata(&path)?; let metadata = std::fs::metadata(&path)?;
let age = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH); let age = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
@@ -546,14 +613,15 @@ async fn get_output_by_filename_from_path(
log_type: LogType, log_type: LogType,
file_name: &str, file_name: &str,
) -> crate::Result<CensoredString> { ) -> crate::Result<CensoredString> {
let logs_folder = match log_type { let path = log_directories_for_type(state, game_dir, log_type)
LogType::InfoLog => state.directories.game_logs_dir(game_dir), .into_iter()
LogType::CrashReport => { .map(|directory| directory.join(file_name))
state.directories.game_crash_reports_dir(game_dir) .find(|path| path.is_file())
} .ok_or_else(|| {
}; crate::ErrorKind::OtherError(format!(
"Log file not found: {file_name}"
let path = logs_folder.join(file_name); ))
})?;
let credentials = Credentials::get_all(&state.pool) let credentials = Credentials::get_all(&state.pool)
.await? .await?
@@ -650,14 +718,15 @@ pub async fn delete_logs_by_filename(
.directories .directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref()); .resolve_game_dir(&instance_path, game_dir_override.as_deref());
let logs_folder = match log_type { let path = log_directories_for_type(&state, &game_dir, log_type)
LogType::InfoLog => state.directories.game_logs_dir(&game_dir), .into_iter()
LogType::CrashReport => { .map(|directory| directory.join(filename))
state.directories.game_crash_reports_dir(&game_dir) .find(|path| path.is_file())
} .ok_or_else(|| {
}; crate::ErrorKind::OtherError(format!(
"Log file not found: {filename}"
let path = logs_folder.join(filename); ))
})?;
io::remove_file(&path).await?; io::remove_file(&path).await?;
Ok(()) Ok(())
} }