From 275a11e3e5345932d9f27e9a4351c4fe5eaf73d8 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Thu, 7 May 2026 10:38:44 +0800 Subject: [PATCH] Prevent startup logs from growing without bounds Long-running tray installs can accumulate an unbounded openless.log. Startup now checks the current log after single-instance arbitration but before simplelog opens the file, then rotates files over 10 MiB to openless.log.1. Normal append logging resumes with a fresh current log and log export continues to copy only openless.log. Rotation remains best-effort: startup continues if archive cleanup or rename fails, but stderr receives an explicit warning so the failure is diagnosable even before the file logger is initialized. Boundary tests cover oversized, small, and missing log files. Constraint: Issue #303 asks for a minimal startup size check with openless.log.1 archive. Constraint: Duplicate macOS/Linux launches must exit before rotation so they cannot rename the owning instance's active log file. Rejected: Add tracing-appender rolling support | new dependency and wider logging rewrite for a low-risk ops fix. Rejected: Add platform-specific rotation-failure tests | brittle across Linux/macOS/Windows file locking semantics. Confidence: high Scope-risk: narrow Directive: Keep export_error_log focused on openless.log; do not include rotated archives unless product explicitly asks for history export. Directive: Keep log rotation best-effort; startup must continue even if archive cleanup or rename fails. Tested: cargo test --manifest-path src-tauri/Cargo.toml log_ Tested: cargo check --manifest-path src-tauri/Cargo.toml Tested: git diff --check Not-tested: Manual startup rotation on Windows/macOS. Related: #303 --- openless-all/app/src-tauri/src/lib.rs | 92 +++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index f8900a44..4a6579a1 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -38,6 +38,8 @@ use std::sync::Arc; #[cfg(target_os = "macos")] use std::time::Duration; +const LOG_ROTATE_LIMIT_BYTES: u64 = 10 * 1024 * 1024; + /// 第一次 show 时把 QA 浮窗摆到屏幕底部居中;之后的 show 不再 reposition, /// 让用户拖动后的位置在 hide → show 之间得以保持。详见 issue #118 v2。 static QA_WINDOW_POSITIONED: AtomicBool = AtomicBool::new(false); @@ -47,9 +49,6 @@ use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, RunEvent, #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - init_file_logger(); - log::info!("=== OpenLess 启动 ==="); - let coordinator = Arc::new(coordinator::Coordinator::new()); let local_asr_download_manager = Arc::new(asr::local::DownloadManager::new()); @@ -76,6 +75,9 @@ pub fn run() { .manage(coordinator.clone()) .manage(local_asr_download_manager.clone()) .setup(move |app| { + init_file_logger(); + log::info!("=== OpenLess 启动 ==="); + // Capsule 启动时定位到屏幕底部居中并隐藏;coordinator 按需显示。 // 与 Swift `CapsuleWindowController.repositionToBottomCenter` 同语义。 if let Some(capsule) = app.get_webview_window("capsule") { @@ -430,6 +432,9 @@ fn init_file_logger() { let log_dir = log_dir_path(); let _ = std::fs::create_dir_all(&log_dir); let log_file = log_dir.join("openless.log"); + if let Err(e) = rotate_log_if_too_large(&log_file) { + eprintln!("[logger] WARN 日志轮转失败: {e}"); + } let config = ConfigBuilder::new().set_time_format_rfc3339().build(); let mut loggers: Vec> = vec![TermLogger::new( LevelFilter::Info, @@ -447,6 +452,23 @@ fn init_file_logger() { let _ = CombinedLogger::init(loggers); } +fn rotate_log_if_too_large(path: &std::path::Path) -> std::io::Result<()> { + let Ok(metadata) = std::fs::metadata(path) else { + return Ok(()); + }; + if metadata.len() <= LOG_ROTATE_LIMIT_BYTES { + return Ok(()); + } + + let archive = path.with_file_name("openless.log.1"); + match std::fs::remove_file(&archive) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + std::fs::rename(path, archive) +} + pub fn log_dir_path() -> std::path::PathBuf { #[cfg(target_os = "macos")] { @@ -837,7 +859,11 @@ fn capsule_height_for_qa() -> f64 { #[cfg(test)] mod tests { - use super::{capsule_height_for_qa, capsule_visual_height, capsule_window_bounds}; + use super::{ + capsule_height_for_qa, capsule_visual_height, capsule_window_bounds, + rotate_log_if_too_large, LOG_ROTATE_LIMIT_BYTES, + }; + use std::io::Write; #[test] fn capsule_window_bounds_leave_room_for_windows_shadow() { @@ -888,4 +914,62 @@ mod tests { #[cfg(not(target_os = "windows"))] assert_eq!(capsule_height_for_qa(), 96.0); } + + #[test] + fn oversized_log_rotates_to_single_archive() { + let dir = std::env::temp_dir().join(format!("openless-log-rotate-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("openless.log"); + let archive = dir.join("openless.log.1"); + + { + let mut file = std::fs::File::create(&log).unwrap(); + file.set_len(LOG_ROTATE_LIMIT_BYTES + 1).unwrap(); + file.write_all(b"x").unwrap(); + } + std::fs::write(&archive, b"old").unwrap(); + + rotate_log_if_too_large(&log).unwrap(); + + assert!(!log.exists()); + assert!(archive.exists()); + assert!(std::fs::metadata(&archive).unwrap().len() > LOG_ROTATE_LIMIT_BYTES); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn small_log_does_not_rotate() { + let dir = std::env::temp_dir().join(format!("openless-log-small-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("openless.log"); + let archive = dir.join("openless.log.1"); + std::fs::write(&log, b"small").unwrap(); + + rotate_log_if_too_large(&log).unwrap(); + + assert!(log.exists()); + assert!(!archive.exists()); + assert_eq!(std::fs::read(&log).unwrap(), b"small"); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn missing_log_does_not_rotate() { + let dir = std::env::temp_dir().join(format!("openless-log-missing-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("openless.log"); + let archive = dir.join("openless.log.1"); + + rotate_log_if_too_large(&log).unwrap(); + + assert!(!log.exists()); + assert!(!archive.exists()); + + let _ = std::fs::remove_dir_all(&dir); + } }