//! 应用全局日志模块 //! 日志文件放在 exe 同目录下,每天一个日志文件 //! 格式:[时间戳] [LEVEL] 消息内容 use chrono::Local; use std::fs::{self, OpenOptions}; use std::io::Write; use std::path::PathBuf; use std::sync::Mutex; #[cfg(target_os = "windows")] use std::sync::atomic::{AtomicBool, Ordering}; /// 全局日志写入锁 static LOG_FILE: Mutex> = Mutex::new(None); /// VEH 是否已注册(防止重复注册) #[cfg(target_os = "windows")] static VEH_REGISTERED: AtomicBool = AtomicBool::new(false); /// 进程启动时间(用于计算运行时间) static PROCESS_START_TIME: std::sync::OnceLock = std::sync::OnceLock::new(); /// 获取 exe 所在目录 fn get_exe_dir() -> PathBuf { if let Ok(exe_path) = std::env::current_exe() { if let Some(parent) = exe_path.parent() { return parent.to_path_buf(); } } std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) } /// 获取日志目录 fn get_log_dir() -> PathBuf { get_exe_dir().join("logs") } /// 获取今天的日志文件路径 fn get_today_log_path() -> Result { let log_dir = get_log_dir(); fs::create_dir_all(&log_dir) .map_err(|e| format!("创建日志目录失败: {}", e))?; let today = Local::now().format("%Y-%m-%d").to_string(); Ok(log_dir.join(format!("app_{}.log", today))) } /// 初始化日志系统 pub fn init_app_logger() -> Result<(), String> { let log_path = get_today_log_path()?; if !log_path.exists() { fs::write(&log_path, "").map_err(|e| format!("创建日志文件失败: {}", e))?; } let mut guard = LOG_FILE.lock().map_err(|e| format!("获取日志锁失败: {}", e))?; *guard = Some(log_path.clone()); let start_msg = format!( "[{}] [INFO] ==================== 应用启动 ====================\n", Local::now().format("%Y-%m-%d %H:%M:%S") ); write_to_log_file(&log_path, &start_msg); Ok(()) } /// 获取当前日志文件路径 pub fn get_current_log_path() -> Option { LOG_FILE .lock() .ok() .and_then(|guard| guard.clone()) .map(|p| p.to_string_lossy().to_string()) } /// 写入日志到文件(不带时间戳前缀,供内部/panic 使用) fn write_to_log_file(path: &PathBuf, content: &str) { if let Ok(mut file) = OpenOptions::new() .create(true) .append(true) .open(path) { let _ = file.write_all(content.as_bytes()); let _ = file.flush(); } } /// 写入带时间戳的日志行 fn write_log_line(level: &str, message: &str) { let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string(); let log_line = format!("[{}] [{}] {}\n", timestamp, level, message); if let Ok(guard) = LOG_FILE.lock() { if let Some(path) = guard.as_ref() { write_to_log_file(path, &log_line); } } eprintln!("{}", log_line.trim()); } /// Info 级别日志 pub fn info(msg: &str) { write_log_line("INFO", msg); } /// Warning 级别日志 pub fn warn(msg: &str) { write_log_line("WARN", msg); } /// Error 级别日志 pub fn error(msg: &str) { write_log_line("ERROR", msg); } /// Debug 级别日志 pub fn debug(msg: &str) { write_log_line("DEBUG", msg); } /// 记录 panic 信息 pub fn log_panic(panic_info: &std::panic::PanicHookInfo) { let location = panic_info .location() .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) .unwrap_or_else(|| "unknown location".to_string()); let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() { s.to_string() } else if let Some(s) = panic_info.payload().downcast_ref::() { s.clone() } else { "Unknown panic payload".to_string() }; let full_msg = format!( "PANIC at {}: {}\nBacktrace:\n{:?}", location, message, std::backtrace::Backtrace::capture() ); write_log_line("PANIC", &full_msg); } /// 设置 panic hook pub fn setup_panic_hook() { std::panic::set_hook(Box::new(|panic_info| { log_panic(&panic_info); })); } // ============================================================ // Windows 顶层异常处理(Vectored Exception Handler) // 能捕获 Rust panic 无法覆盖的崩溃,如访问违规、栈溢出等 // ============================================================ #[cfg(target_os = "windows")] const EXCEPTION_ACCESS_VIOLATION: u32 = 0xC0000005; #[cfg(target_os = "windows")] const EXCEPTION_STACK_OVERFLOW: u32 = 0xC00000FD; #[cfg(target_os = "windows")] const EXCEPTION_INT_DIVIDE_BY_ZERO: u32 = 0xC0000094; #[cfg(target_os = "windows")] const EXCEPTION_ILLEGAL_INSTRUCTION: u32 = 0xC000001D; #[cfg(target_os = "windows")] const EXCEPTION_BREAKPOINT: u32 = 0x80000003; #[cfg(target_os = "windows")] fn exception_code_to_name(code: u32) -> &'static str { match code { 0xC0000005 => "ACCESS_VIOLATION", 0xC00000FD => "STACK_OVERFLOW", 0xC0000094 => "INT_DIVIDE_BY_ZERO", 0xC000001D => "ILLEGAL_INSTRUCTION", 0x80000003 => "BREAKPOINT", 0xC0000008 => "INVALID_HANDLE", 0xC000001E => "LOCK_VIOLATION", 0xC0000025 => "NOT_MAPPED_VIEW", 0xC000000D => "DATA_TYPE_MISALIGNMENT", 0xC0000409 => "STACK_BUFFER_OVERRUN", 0xE0000001 => "GUARD_PAGE_VIOLATION", 0xC0000374 => "HEAP_CORRUPTION", 0xC0000142 => "DLL_NOT_FOUND", 0xC0000135 => "ENTRYPOINT_NOT_FOUND", _ => "UNKNOWN", } } #[cfg(target_os = "windows")] fn format_windows_exception_record(exception_pointers: *mut std::ffi::c_void) -> String { use std::ffi::c_void; // LPEXCEPTION_POINTERS 结构: // typedef struct _EXCEPTION_POINTERS { // PEXCEPTION_RECORD ExceptionRecord; // 偏移 0 // PCONTEXT ContextRecord; // 偏移 8 (64位) // } EXCEPTION_POINTERS; let exc_record_ptr = unsafe { *(exception_pointers as *const *const c_void) }; if exc_record_ptr.is_null() { return "ExceptionRecord is null".to_string(); } // EXCEPTION_RECORD 结构 (前几个字段): // DWORD ExceptionCode; // 偏移 0, 4字节 // DWORD ExceptionFlags; // 偏移 4, 4字节 // struct _EXCEPTION_RECORD *ExceptionRecord; // 偏移 8, 8字节 (64位) // PVOID ExceptionAddress; // 偏移 16, 8字节 (64位) // DWORD NumberParameters; // 偏移 24, 4字节 // ULONG_PTR ExceptionInformation[15]; // 偏移 32 let exc_code = unsafe { *(exc_record_ptr as *const u32) }; let exc_flags = unsafe { *((exc_record_ptr as *const u32).offset(1)) }; let exc_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(2)) }; let mut info = format!( "ExceptionCode=0x{:08X} ({})\n ExceptionFlags=0x{:08X}\n ExceptionAddress={:?}", exc_code, exception_code_to_name(exc_code), exc_flags, exc_addr ); if exc_code == EXCEPTION_ACCESS_VIOLATION { // ExceptionInformation[0]: 0 = read, 1 = write // ExceptionInformation[1]: 访问的虚拟地址 let read_write = unsafe { *((exc_record_ptr as *const u64).offset(4)) }; let virt_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(5)) }; info.push_str(&format!( "\n Attempted {} at address {:?}", if read_write == 0 { "read" } else { "write" }, virt_addr )); } info } #[cfg(target_os = "windows")] extern "system" fn veh_exception_handler( exception_pointers: *mut std::ffi::c_void, ) -> i32 { use std::ffi::c_void; // LPEXCEPTION_POINTERS 结构 let exc_record_ptr = unsafe { *(exception_pointers as *const *const c_void) }; if exc_record_ptr.is_null() { // 记录基本信息 let msg = format!( "==================== WINDOWS CRASH ====================\n\ ExceptionRecord is null\n\ ProcessInfo:\n\ PID={}\n\ ThreadId={:?}\n\ ========================================================\n", std::process::id(), std::thread::current().id() ); eprintln!("{}", msg); return 0; // EXCEPTION_CONTINUE_SEARCH } let exc_code = unsafe { *(exc_record_ptr as *const u32) }; let exc_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(2)) }; let record_str = format_windows_exception_record(exception_pointers); let backtrace = format!("{:?}", std::backtrace::Backtrace::capture()); let msg = format!( "==================== WINDOWS CRASH ====================\n\ ExceptionCode=0x{:08X} ({})\n\ ExceptionAddress={:?}\n\ RecordInfo:\n{}\n\ Backtrace:\n{}\n\ ProcessInfo:\n\ PID={}\n\ ThreadId={:?}\n\ ========================================================\n", exc_code, exception_code_to_name(exc_code), exc_addr, record_str, backtrace, std::process::id(), std::thread::current().id() ); if let Ok(guard) = LOG_FILE.lock() { if let Some(path) = guard.as_ref() { let _ = write_to_log_file(path, &msg); } } eprintln!("{}", msg); // 继续执行,让 Rust 的 panic hook 也有机会处理 // EXCEPTION_CONTINUE_SEARCH = 0(交给其他处理器) // EXCEPTION_EXECUTE_HANDLER = 1(执行我们的处理然后终止) // 这里返回 CONTINUE_SEARCH,让其他处理器(包括 Rust)处理 // 但如果是非常严重的错误,可以返回 EXECUTE_HANDLER if exc_code == EXCEPTION_STACK_OVERFLOW || exc_code == EXCEPTION_ACCESS_VIOLATION { // 栈溢出和访问违规非常危险,返回 EXECUTE_HANDLER // 让程序优雅退出,而不是继续执行导致不可预测行为 1 // EXCEPTION_EXECUTE_HANDLER } else { 0 // EXCEPTION_CONTINUE_SEARCH } } /// 注册 Windows 顶层异常处理器(Vectored Exception Handler) /// 这能捕获一些 Rust panic 无法覆盖的崩溃,如: /// - Access Violation(访问违规) /// - Stack Overflow(栈溢出) /// - 非法指令 /// - 除零错误 #[cfg(target_os = "windows")] pub fn setup_windows_exception_handler() { if VEH_REGISTERED.swap(true, Ordering::SeqCst) { return; // 已经注册过了 } use std::ffi::c_void; // AddVectoredExceptionHandler 来自 winapi // RUSTSEC-2023-0071: 使用 winapi 而非 libc // 函数签名:unsafe extern "system" fn(LPEXCEPTION_POINTERS) -> LONG // 返回值:EXCEPTION_CONTINUE_SEARCH(0) 或 EXCEPTION_EXECUTE_HANDLER(1) #[link(name = "kernel32")] extern "system" { fn AddVectoredExceptionHandler( first_handler: u32, // 1 = CALL_FIRST, 0 = CALL_LAST handler: *mut std::ffi::c_void, ) -> *mut std::ffi::c_void; } unsafe { let result = AddVectoredExceptionHandler(1, veh_exception_handler as *mut c_void); if result.is_null() { if let Ok(guard) = LOG_FILE.lock() { if let Some(path) = guard.as_ref() { let msg = "[VEH] 注册 Windows 顶层异常处理器失败\n"; let _ = write_to_log_file(path, msg); } } } else { if let Ok(guard) = LOG_FILE.lock() { if let Some(path) = guard.as_ref() { let msg = "[VEH] Windows 顶层异常处理器注册成功\n"; let _ = write_to_log_file(path, msg); } } } } } /// 非 Windows 平台的空实现 #[cfg(not(target_os = "windows"))] pub fn setup_windows_exception_handler() { // no-op on non-Windows } /// 设置所有崩溃处理机制 pub fn setup_all_crash_handlers() { setup_panic_hook(); setup_windows_exception_handler(); } // ============================================================ // 扫货相关日志 // ============================================================ /// 扫货任务开始日志 pub fn log_sweep_start(sweep_request_id: i64, selection_request_id: i64, worker_count: u32) { info(&format!( "[SWEEP-START] sweep_request_id={} selection_request_id={} worker_count={}", sweep_request_id, selection_request_id, worker_count )); } /// 扫货任务结束日志 pub fn log_sweep_end(sweep_request_id: i64, status: &str) { info(&format!( "[SWEEP-END] sweep_request_id={} status={}", sweep_request_id, status )); } /// 扫货轮次日志 pub fn log_sweep_round(sweep_request_id: i64, round: u64, items_count: usize) { info(&format!( "[SWEEP-ROUND] sweep_request_id={} round={} items_count={}", sweep_request_id, round, items_count )); } /// API 调用开始日志 pub fn log_api_call_start(api_name: &str, url: &str) { debug(&format!("[API-START] {} {}", api_name, url)); } /// API 调用结束日志 pub fn log_api_call_end(api_name: &str, status: u16, elapsed_ms: u64) { debug(&format!( "[API-END] {} status={} elapsed={}ms", api_name, status, elapsed_ms )); } /// API 调用错误日志 pub fn log_api_error(api_name: &str, err_msg: &str) { write_log_line("ERROR", &format!("[API-ERROR] {}: {}", api_name, err_msg)); } /// Steam 账号操作日志 pub fn log_steam_account_op(account: &str, op: &str, result: &str) { info(&format!( "[STEAM-ACCOUNT] account={} op={} result={}", account, op, result )); } /// 购买成功日志 pub fn log_purchase_success( goods_id: &str, goods_name: &str, listing_id: &str, price_usd: f64, account: &str, ) { info(&format!( "[PURCHASE-SUCCESS] goods_id={} goods_name={} listing_id={} price=${} account={}", goods_id, goods_name, listing_id, price_usd, account )); } /// 购买失败日志 pub fn log_purchase_failure( goods_id: &str, goods_name: &str, listing_id: &str, reason: &str, ) { warn(&format!( "[PURCHASE-FAIL] goods_id={} goods_name={} listing_id={} reason={}", goods_id, goods_name, listing_id, reason )); } /// 代理切换日志 pub fn log_proxy_change(proxy_endpoint: &str, reason: &str) { debug(&format!( "[PROXY-CHANGE] endpoint={} reason={}", proxy_endpoint, reason )); } /// 心跳日志 pub fn log_heartbeat(status: &str) { debug(&format!("[HEARTBEAT] status={}", status)); } /// 线程/任务开始 pub fn log_task_start(task_name: &str) { debug(&format!("[TASK-START] {}", task_name)); } /// 线程/任务结束 pub fn log_task_end(task_name: &str) { debug(&format!("[TASK-END] {}", task_name)); } // ============================================================ // 内存和资源监控诊断日志 // ============================================================ /// 获取当前进程的内存使用情况(Windows) #[cfg(target_os = "windows")] pub fn get_memory_usage_mb() -> f64 { use std::mem::MaybeUninit; #[repr(C)] struct MEMORYSTATUSEX { dw_length: u32, dw_memory_load: u32, ull_total_phys: u64, ull_avail_phys: u64, ull_total_page_file: u64, ull_avail_page_file: u64, ull_total_virtual: u64, ull_avail_virtual: u64, ull_avail_extended_virtual: u64, } #[link(name = "kernel32")] extern "system" { fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32; } let mut mem_status = MaybeUninit::::uninit(); let mem_status_ptr = mem_status.as_mut_ptr(); // 初始化 dw_length 字段 unsafe { (*mem_status_ptr).dw_length = std::mem::size_of::() as u32; } unsafe { if GlobalMemoryStatusEx(mem_status_ptr) != 0 { let status = mem_status.assume_init(); // 返回已使用的物理内存(MB) ((status.ull_total_phys - status.ull_avail_phys) as f64) / (1024.0 * 1024.0) } else { 0.0 } } } /// 获取当前进程的内存使用情况(非 Windows 平台) #[cfg(not(target_os = "windows"))] pub fn get_memory_usage_mb() -> f64 { 0.0 } /// 获取当前进程的总内存(MB) #[cfg(target_os = "windows")] pub fn get_total_memory_mb() -> f64 { use std::mem::MaybeUninit; #[repr(C)] struct MEMORYSTATUSEX { dw_length: u32, dw_memory_load: u32, ull_total_phys: u64, ull_avail_phys: u64, ull_total_page_file: u64, ull_avail_page_file: u64, ull_total_virtual: u64, ull_avail_virtual: u64, ull_avail_extended_virtual: u64, } #[link(name = "kernel32")] extern "system" { fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32; } let mut mem_status = MaybeUninit::::uninit(); let mem_status_ptr = mem_status.as_mut_ptr(); unsafe { (*mem_status_ptr).dw_length = std::mem::size_of::() as u32; } unsafe { if GlobalMemoryStatusEx(mem_status_ptr) != 0 { let status = mem_status.assume_init(); (status.ull_total_phys as f64) / (1024.0 * 1024.0) } else { 0.0 } } } #[cfg(not(target_os = "windows"))] pub fn get_total_memory_mb() -> f64 { 0.0 } /// 获取当前活跃的线程数量 pub fn get_thread_count() -> usize { std::thread::available_parallelism() .map(|p| p.get()) .unwrap_or(0) } /// 获取当前进程运行时间(秒) pub fn get_process_uptime_secs() -> u64 { let start = PROCESS_START_TIME.get_or_init(|| std::time::Instant::now()); start.elapsed().as_secs() } /// 记录完整的系统资源诊断日志 pub fn log_resource_diagnostics(tag: &str) { let mem_used = get_memory_usage_mb(); let mem_total = get_total_memory_mb(); let mem_percent = if mem_total > 0.0 { (mem_used / mem_total * 100.0).min(100.0) } else { 0.0 }; let thread_count = get_thread_count(); let uptime = get_process_uptime_secs(); let uptime_formatted = format_uptime(uptime); info(&format!( "[DIAGNOSTIC][{}] 内存: {:.1}/{:.0}MB ({:.1}%), 线程: {}, 运行时间: {}", tag, mem_used, mem_total, mem_percent, thread_count, uptime_formatted )); } /// 格式化运行时间 fn format_uptime(secs: u64) -> String { let hours = secs / 3600; let minutes = (secs % 3600) / 60; let seconds = secs % 60; if hours > 0 { format!("{}h{}m{}s", hours, minutes, seconds) } else if minutes > 0 { format!("{}m{}s", minutes, seconds) } else { format!("{}s", seconds) } } /// 记录代理池状态诊断日志 pub fn log_proxy_pool_status(tag: &str, pool_size: usize, in_use: usize) { info(&format!( "[DIAGNOSTIC][{}] 代理池: 可用={}, 使用中={}, 空闲={}", tag, pool_size, in_use, pool_size.saturating_sub(in_use) )); } /// 记录 Worker 状态诊断日志 pub fn log_worker_status(tag: &str, worker_index: u32, active_tasks: usize) { info(&format!( "[DIAGNOSTIC][{}] Worker[{}]: 活跃任务={}", tag, worker_index, active_tasks )); } /// 记录扫货进度诊断日志(包含详细统计) pub fn log_sweep_progress_diagnostic( sweep_request_id: i64, round: u64, items_processed: u64, purchases_success: u64, purchases_failed: u64, proxy_pool_available: usize, proxy_pool_in_use: usize, ) { let mem_used = get_memory_usage_mb(); let thread_count = get_thread_count(); info(&format!( "[DIAGNOSTIC][PROGRESS] sweep={} round={} processed={} success={} failed={} mem={:.0}MB threads={} proxies=可用{}/使用中{}", sweep_request_id, round, items_processed, purchases_success, purchases_failed, mem_used, thread_count, proxy_pool_available, proxy_pool_in_use )); } #[cfg(test)] mod tests { use super::*; #[test] fn test_log_dir_creation() { let log_dir = get_log_dir(); assert!(log_dir.ends_with("logs")); } #[test] fn test_memory_functions() { let mem_used = get_memory_usage_mb(); let mem_total = get_total_memory_mb(); assert!(mem_used >= 0.0); assert!(mem_total >= 0.0); assert!(mem_used <= mem_total); } #[test] fn test_uptime_format() { assert_eq!(format_uptime(30), "30s"); assert_eq!(format_uptime(90), "1m30s"); assert_eq!(format_uptime(3660), "1h1m0s"); } }