// ============================================================ // 主进程日志核心 — 文件写入 / 轮转 / 清理 // // 架构: // - 日志写入 {userData}/logs/app-YYYY-MM-DD.log(JSON Lines 格式) // - 每日轮转:日期变化时自动创建新文件 // - 保留策略:7 天前旧文件自动删除 // - 写入队列批量冲刷(setImmediate 聚合同一 tick 的日志) // - 惰性初始化:app.getPath('userData') 在 ready 后才可用, // initLogger() 在 whenReady 中显式调用预热 // // 用法(主进程): // import { logger, initLogger } from './main/logger'; // logger.info('updater', '检查更新', { version: '0.1.0' }); // // 渲染进程日志通过 log-ipc.ts IPC 通道写入,调用 writeRendererLog() // ============================================================ import { app } from 'electron'; import { createWriteStream, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; import type { WriteStream } from 'node:fs'; import type { LogEntry, LogLevel, LogCategory, LogErrorSnapshot } from '../../shared/types/logging'; import { sanitizeLogEntry } from '../../shared/utils/sanitize'; // ---------- 内部状态 ---------- let writeStream: WriteStream | null = null; let currentDate = ''; let logDir = ''; let logDirResolver: (() => string) | null = null; const writeQueue: string[] = []; let isWriting = false; const MAX_DAYS = 7; // ---------- 辅助函数 ---------- function getDateStr(): string { return new Date().toISOString().slice(0, 10); } function getLogFilePath(date: string): string { return join(logDir, `app-${date}.log`); } /** 确保 logs/ 目录存在 */ function ensureDir(): void { if (logDir) return; try { logDir = logDirResolver ? logDirResolver() : join(app.getPath('userData'), 'logs'); } catch { // app.getPath 尚不可用(app.ready 之前),静默跳过 return; } if (!logDir) return; try { mkdirSync(logDir, { recursive: true }); } catch { /* 目录创建失败 — 后续写入静默丢弃 */ } } /** 日期轮转:日切时关闭旧流 → 创建新文件 → 清理过期文件 */ function rotateIfNeeded(): void { const today = getDateStr(); if (currentDate === today && writeStream) return; if (writeStream) { try { writeStream.end(); } catch { /* ignore */ } writeStream = null; } try { writeStream = createWriteStream(getLogFilePath(today), { flags: 'a' }); currentDate = today; } catch { writeStream = null; } purgeOldLogs(); } /** 清理超过 MAX_DAYS 天的旧日志文件 */ function purgeOldLogs(): void { if (!logDir) return; const now = Date.now(); const maxAge = MAX_DAYS * 24 * 60 * 60 * 1000; try { const files = readdirSync(logDir); for (const file of files) { if (!file.startsWith('app-') || !file.endsWith('.log')) continue; try { const stat = statSync(join(logDir, file)); if (now - stat.mtimeMs > maxAge) { unlinkSync(join(logDir, file)); } } catch { /* skip inaccessible files */ } } } catch { /* 目录读取失败 — 下次轮转再试 */ } } /** 批量冲刷写入队列 */ function processQueue(): void { if (isWriting || writeQueue.length === 0) return; isWriting = true; ensureDir(); if (logDir) { rotateIfNeeded(); if (writeStream) { const batch = writeQueue.splice(0).join(''); try { writeStream.write(batch); } catch { /* 写入失败 — 数据丢失,但日志不应阻断业务 */ } } else { writeQueue.length = 0; } } else { // app ready 前尚无 userData 路径 — 丢弃(此时距 ready 极近) writeQueue.length = 0; } isWriting = false; } /** 错误对象 → 可序列化快照 */ function errorToSnapshot(err: Error): LogErrorSnapshot { return { name: err.name, message: err.message, stack: err.stack, cause: (err as { cause?: unknown }).cause ? String((err as { cause?: unknown }).cause) : undefined, }; } /** 构建日志条目 */ function createEntry( level: LogLevel, category: LogCategory, message: string, processType: 'main' | 'renderer', error?: Error, context?: Record, ): LogEntry { const entry: LogEntry = { timestamp: new Date().toISOString(), level, category, message, processType, }; if (context && Object.keys(context).length > 0) { entry.context = context; } if (error) { entry.error = errorToSnapshot(error); } return entry; } /** 内部写入入口 */ function write( level: LogLevel, category: LogCategory, message: string, error?: Error, context?: Record, ): void { const entry = sanitizeLogEntry( createEntry(level, category, message, 'main', error, context), ); writeQueue.push(JSON.stringify(entry) + '\n'); // 首个入队项触发 setImmediate 批量冲刷 if (writeQueue.length === 1) { setImmediate(processQueue); } } // ---------- 对外 API ---------- /** 主进程日志器(与渲染进程 API 完全一致) */ export const logger = { debug(category: LogCategory, message: string, context?: Record): void { write('DEBUG', category, message, undefined, context); }, info(category: LogCategory, message: string, context?: Record): void { write('INFO', category, message, undefined, context); }, warn( category: LogCategory, message: string, error?: Error, context?: Record, ): void { write('WARN', category, message, error, context); }, error( category: LogCategory, message: string, error?: Error, context?: Record, ): void { write('ERROR', category, message, error, context); }, }; /** 注入自定义日志目录解析器(在 initLogger() 之前调用) */ export function setLogDirResolver(resolver: () => string): void { logDirResolver = resolver; } /** 预热日志模块(在 app.whenReady() 中调用) */ export function initLogger(): void { ensureDir(); if (logDir) { rotateIfNeeded(); } } /** * 接收渲染进程日志并写入文件 * 由 log-ipc.ts 的 IPC 监听器调用 */ export function writeRendererLog(entry: LogEntry): void { writeQueue.push(JSON.stringify(entry) + '\n'); if (writeQueue.length === 1) { setImmediate(processQueue); } } /** 冲刷所有未写入日志并关闭写入流(在 app.before-quit 中调用) */ export function flushLogger(): void { if (writeQueue.length > 0) { processQueue(); } if (writeStream) { try { writeStream.end(); } catch { /* ignore */ } writeStream = null; } }