Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 56 additions & 29 deletions src-tauri/src/commands/file_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const ZIP_CENTRAL_DIR_FIXED_BYTES: u64 = 46;
const MAX_EXPORTS_TO_KEEP: usize = 10;
/// 需要随日志一起清空的调试产物子目录(内容由 MaaFW 的 save_on_error / save_draw 写入)。
const DEBUG_ARTIFACT_DIRS: [&str; 2] = ["on_error", "vision"];
/// 日志目录名(位于应用数据目录下)。
const DEBUG_DIR: &str = "debug";
/// 日志导出产物目录名,与 `debug/` 同级,避免下次导出把上次的产物扫进去。
const DEBUG_EXPORTS_DIR: &str = "debug_exports";

#[derive(Clone)]
struct ExportEntry {
Expand Down Expand Up @@ -296,10 +300,10 @@ pub fn get_data_dir() -> Result<String, String> {
Ok(data_dir.to_string_lossy().to_string())
}

/// 清空调试产物目录内容,保留目录本身。返回成功删除的条目数。
/// 清空目录内容,保留目录本身。返回成功删除的条目数(子目录整体计 1 条)
///
/// 保留目录本身是因为 MaaFW 的 save_on_error 直接往 `on_error/` 写文件,父目录缺失会写入失败
fn clear_debug_artifact_dir(dir: &Path) -> u64 {
/// 保留根目录只是为了不改动既有目录结构;MaaFW 写调试产物前会自建父目录,缺失也不会写入失败
fn clear_dir_contents(dir: &Path) -> u64 {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
Expand All @@ -326,26 +330,28 @@ fn clear_debug_artifact_dir(dir: &Path) -> u64 {
deleted
}

/// 删除 debug 目录中的 .log 文件,并清空 on_error/、vision/ 内的调试产物(保留这两个目录本身)。
/// 可选择排除一个当前正在使用的日志文件。返回删除的文件与调试产物总数。
#[tauri::command]
pub fn clear_log_files(exclude_file_name: Option<String>) -> Result<u64, String> {
let debug_dir = get_app_data_dir()?.join("debug");

if !debug_dir.exists() {
return Ok(0);
}
/// 递归删除 `dir` 及其所有子目录下的 .log 文件,返回删除数量。
/// `exclude_file_name` 匹配的文件名会被跳过(当前会话正在写入的日志)。
/// 只删文件不回收空目录,避免改动既有目录结构。
fn remove_log_files_recursively(dir: &Path, exclude_file_name: Option<&str>) -> u64 {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
log::debug!("Failed to read log dir [{}]: {}", dir.display(), e);
return 0;
}
};

let mut deleted = 0_u64;
let entries = std::fs::read_dir(&debug_dir)
.map_err(|e| format!("读取日志目录失败 [{}]: {}", debug_dir.display(), e))?;

for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();

if path.is_dir() {
deleted =
deleted.saturating_add(remove_log_files_recursively(&path, exclude_file_name));
Comment on lines +349 to +351

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): path.is_dir() 会跟随目录符号链接,因此 debug 内部的符号链接会导致递归遍历进入符号链接的目标,并删除应用程序 debug 目录之外匹配的 .log 文件。

触发条件: debug 下存在意外的或用户创建的目录符号链接时。

建议修复: 使用支持符号链接识别的元数据检查条目,并跳过符号链接,而不是递归进入其中。

Suggested change
if path.is_dir() {
deleted =
deleted.saturating_add(remove_log_files_recursively(&path, exclude_file_name));
let Ok(metadata) = std::fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_symlink() {
continue;
}
if metadata.is_dir() {
deleted =
deleted.saturating_add(remove_log_files_recursively(&path, exclude_file_name));
Original comment in English

🚨 issue (security): path.is_dir() follows directory symlinks, so a symlink inside debug causes the recursive traversal to enter the symlink target and delete matching .log files outside the application's debug directory.

Triggers: When an unexpected or user-created directory symlink exists below debug.

Suggested fix: Inspect the entry with symlink-aware metadata and skip symlinks instead of recursing into them.

Suggested change
if path.is_dir() {
deleted =
deleted.saturating_add(remove_log_files_recursively(&path, exclude_file_name));
let Ok(metadata) = std::fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_symlink() {
continue;
}
if metadata.is_dir() {
deleted =
deleted.saturating_add(remove_log_files_recursively(&path, exclude_file_name));

continue;
}

if !path.is_file() {
continue;
}
Expand All @@ -354,11 +360,7 @@ pub fn clear_log_files(exclude_file_name: Option<String>) -> Result<u64, String>
continue;
};

if !name.ends_with(".log") {
continue;
}

if exclude_file_name.as_deref() == Some(name) {
if !name.ends_with(".log") || exclude_file_name == Some(name) {
continue;
}
Comment on lines +363 to 365

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (bug_risk): 排除逻辑仅基于文件名,因此与活动根日志文件同名的嵌套日志也会被跳过。因此,当两个目录包含同名日志文件时,递归清理会留下无关的过期日志。

触发条件: 嵌套的 debug 目录包含一个与当前会话日志同名但实际不同的日志文件时。

建议修复: 传递并比较活动日志的规范路径/完整路径,或者将基于文件名的排除限制在已知的活动日志位置。

Original comment in English

nitpick (bug_risk): The exclusion is based only on the basename, so a nested log with the same filename as the active root log is skipped as well. Recursive cleanup therefore leaves unrelated stale logs whenever two directories contain the same log filename.

Triggers: When a nested debug directory contains a different log file with the same basename as the current session's log.

Suggested fix: Pass and compare the active log's canonical/full path, or restrict the basename exclusion to the known active-log location.


Expand All @@ -368,15 +370,40 @@ pub fn clear_log_files(exclude_file_name: Option<String>) -> Result<u64, String>
}
}

deleted
}

/// `clear_log_files` 的可测核心,接收具体目录而不依赖应用数据目录。
fn clear_log_dirs(debug_dir: &Path, exports_dir: &Path, exclude_file_name: Option<&str>) -> u64 {
let mut deleted = remove_log_files_recursively(debug_dir, exclude_file_name);

for dir_name in DEBUG_ARTIFACT_DIRS {
let artifact_dir = debug_dir.join(dir_name);
if !artifact_dir.is_dir() {
continue;
}
deleted = deleted.saturating_add(clear_debug_artifact_dir(&artifact_dir));
deleted = deleted.saturating_add(clear_dir_contents(&artifact_dir));
}

Ok(deleted)
if exports_dir.is_dir() {
deleted = deleted.saturating_add(clear_dir_contents(exports_dir));
Comment on lines +388 to +389

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): clear_log_dirs 会删除 debug_exports 下的每个条目,但没有与 export_logs_blocking 进行协调。如果用户在导出正在写入时清除日志,remove_dir_all 会删除当前活动的导出目录,随后导出器在创建或完成 ZIP 时会失败,并可能返回错误或指向已删除文件的路径。

触发条件: 手动清除日志的操作与正在进行的日志导出重叠时。

建议修复: 使用共享锁串行化清除和导出操作,或者避免在导出完成前删除当前活动的导出目录。

Original comment in English

issue (bug_risk): clear_log_dirs deletes every entry under debug_exports without coordinating with export_logs_blocking. If the user clears logs while an export is being written, remove_dir_all removes the active export directory and the exporter then fails while creating or finishing a ZIP, potentially returning an error or a path to a deleted file.

Triggers: When a manual log clear overlaps an in-progress log export.

Suggested fix: Serialize clearing and exporting with a shared lock, or avoid deleting the currently active export directory until the export completes.

}

deleted
}

/// 递归删除 debug 目录(含所有子目录)中的 .log 文件,清空 on_error/、vision/ 内的调试产物
/// 以及 debug_exports/ 下的日志导出产物(保留这几个目录本身)。
/// 可选择排除一个当前正在使用的日志文件。返回删除的文件与产物总数。
#[tauri::command]
pub fn clear_log_files(exclude_file_name: Option<String>) -> Result<u64, String> {
let data_dir = get_app_data_dir()?;

Ok(clear_log_dirs(
&data_dir.join(DEBUG_DIR),
&data_dir.join(DEBUG_EXPORTS_DIR),
exclude_file_name.as_deref(),
))
}

/// 获取当前工作目录
Expand Down Expand Up @@ -573,7 +600,7 @@ fn export_logs_blocking(

// 日志在数据目录下(macOS: ~/Library/Application Support/MXU/debug)
let data_dir = get_app_data_dir()?;
let debug_dir = data_dir.join("debug");
let debug_dir = data_dir.join(DEBUG_DIR);

if !debug_dir.exists() {
return Err("日志目录不存在".to_string());
Expand All @@ -589,7 +616,7 @@ fn export_logs_blocking(
format!("{}-logs-{}-{}", name, version, date_str)
};
// 产物放在 debug_exports/ 下而不是 debug/,避免下次导出把上次的产物扫进去。
let exports_root = data_dir.join("debug_exports");
let exports_root = data_dir.join(DEBUG_EXPORTS_DIR);
let out_dir = exports_root.join(&dir_name);
std::fs::create_dir_all(&out_dir)
.map_err(|e| format!("创建导出目录失败 [{}]: {}", out_dir.display(), e))?;
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export default {
resetWindowLayoutHint: 'Restore window size to default and center the window',
autoClearLogsOnLaunch: 'Auto-clear Runtime Logs',
autoClearLogsOnLaunchHint:
'Automatically clear runtime logs and delete old log files along with debug screenshots in on_error and vision every time the project is launched',
'Automatically clear runtime logs and debug files every time the project is launched',
},

// Special tasks
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/locales/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export default {
resetWindowLayoutHint: 'ウィンドウサイズをデフォルトに戻し、中央に配置します',
autoClearLogsOnLaunch: '実行ログの自動クリア',
autoClearLogsOnLaunchHint:
'プロジェクトの起動時に自動で実行ログをクリアし、古いログファイルと on_error・vision 内のデバッグスクリーンショットを削除します',
'プロジェクトの起動時に実行ログとデバッグファイルを自動でクリアします',
},

// 特殊タスク
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/locales/ko-KR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ export default {
resetWindowLayout: '창 레이아웃 초기화',
resetWindowLayoutHint: '창 크기를 기본값으로 복원하고 화면 중앙에 배치합니다',
autoClearLogsOnLaunch: '로그 자동 지우기',
autoClearLogsOnLaunchHint:
'프로젝트를 시작할 때 런타임 로그를 자동으로 지우고 이전 로그 파일과 on_error, vision 폴더의 디버그 스크린샷을 삭제합니다',
autoClearLogsOnLaunchHint: '프로젝트를 시작할 때 런타임 로그와 디버그 파일을 자동으로 지웁니다',
},

// 특수 작업
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ export default {
resetWindowLayout: '重置窗口布局',
resetWindowLayoutHint: '将窗口大小恢复为默认值,并居中显示',
autoClearLogsOnLaunch: '自动清理运行日志',
autoClearLogsOnLaunchHint:
'每次启动项目时,自动清理运行日志,并删除旧的日志文件与 on_error、vision 目录下的调试截图',
autoClearLogsOnLaunchHint: '每次启动项目时,自动清理运行日志与调试文件',
},

// 特殊任务
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@ export default {
resetWindowLayout: '重設視窗佈局',
resetWindowLayoutHint: '將視窗大小恢復為預設值,並置中顯示',
autoClearLogsOnLaunch: '自動清理運行日誌',
autoClearLogsOnLaunchHint:
'每次啟動項目時,自動清理運行日誌,並刪除舊的日誌檔案與 on_error、vision 目錄下的除錯截圖',
autoClearLogsOnLaunchHint: '每次啟動項目時,自動清理運行日誌與除錯檔案',
},

// 特殊任務
Expand Down