import 'dart:io'; /// 本地文件日志:按天滚动(app-20260921.log)、单文件 5MB 轮转加序号、 /// 启动时清理 7 天前旧文件。供用户把日志文件发回做现场分析。 /// /// 全同步追加写(本应用日志低频,性能无碍;同步写保证异常退出前内容已落盘)。 /// 设计为静态工具而非对象:pavo 的 isolate 入口是顶层函数(不能捕获实例, /// 见 pavo_backend 的 unsendable 教训),只把 [currentPath](纯数据)传进 /// isolate,isolate 内通过 [writeToPath] 直接追加。 class FileLog { static const _keepDays = 7; static const _maxBytes = 5 * 1024 * 1024; static String? _dir; static String? _currentPath; /// 日志目录:mac ~/Library/Logs/zax-desktop-plugin/;win %APPDATA%\zax-desktop-plugin\logs\ /// 推导失败(无 HOME/APPDATA)时禁用文件日志,仅内存/stdout。 static String? defaultDir() { final env = Platform.environment; if (Platform.isMacOS) { final home = env['HOME']; return home == null ? null : '$home/Library/Logs/zax-desktop-plugin'; } if (Platform.isWindows) { final appData = env['APPDATA']; return appData == null ? null : '$appData\\zax-desktop-plugin\\logs'; } return null; } /// 初始化(main 最早调用):建目录、清理过期文件、定位当天日志文件。 /// [dir] 可注入目录(测试用),默认按平台推导。 static void init([String? dir]) { final resolved = dir ?? defaultDir(); if (resolved == null) return; try { Directory(resolved).createSync(recursive: true); _dir = resolved; _cleanupOld(); _locateCurrent(); } catch (_) { // 目录不可写等:文件日志禁用,不影响应用运行 _dir = null; } } static String? get dir => _dir; static String? get currentPath => _currentPath; static String _dayStamp([DateTime? now]) { final t = now ?? DateTime.now(); String p2(int v) => v.toString().padLeft(2, '0'); return '${t.year}${p2(t.month)}${p2(t.day)}'; } static String _stampLine(String line) { final now = DateTime.now(); String p2(int v) => v.toString().padLeft(2, '0'); return '${_dayStamp(now)} ${p2(now.hour)}:${p2(now.minute)}:${p2(now.second)} $line'; } static void _locateCurrent() { final base = '$_dir/app-${_dayStamp()}'; var path = '$base.log'; var seq = 1; // 当天文件超限时轮转 app-20260921-1.log、-2.log…(文件可能尚不存在,先判存在) while (File(path).existsSync() && File(path).lengthSync() >= _maxBytes) { path = '$base-${seq++}.log'; } _currentPath = path; } /// 追加一行到当前日志文件(带完整时间戳)。任何 IO 异常自吞—— /// 日志永远不能影响主流程。写入超限时轮转到下一个序号文件。 static void write(String line) { final path = _currentPath; if (path == null) return; writeToPath(path, line); try { if (File(path).lengthSync() >= _maxBytes) { _locateCurrent(); } } catch (_) {} } /// 追加一行到指定日志文件(isolate 内使用:路径以纯数据传入)。 static void writeToPath(String path, String line) { try { File(path).writeAsStringSync('${_stampLine(line)}\n', mode: FileMode.append); } catch (_) {} } /// 重置状态(测试用;运行期无需调用——静态路径常驻,多实例写同一文件为追加语义)。 static void dispose() { _dir = null; _currentPath = null; } /// 清理 7 天前的 app-*.log(按文件名中的日期判断,与按天滚动语义一致; /// 名字里解析不出日期的文件跳过不动)。 static void _cleanupOld() { final cutoff = DateTime.now().subtract(const Duration(days: _keepDays)); try { for (final entity in Directory(_dir!).listSync()) { if (entity is! File) continue; final name = entity.uri.pathSegments.last; final m = RegExp(r'^app-(\d{8})(?:-\d+)?\.log$').firstMatch(name); if (m == null) continue; final y = int.parse(m.group(1)!.substring(0, 4)); final mo = int.parse(m.group(1)!.substring(4, 6)); final d = int.parse(m.group(1)!.substring(6, 8)); if (DateTime(y, mo, d).isBefore(cutoff)) { entity.deleteSync(); } } } catch (_) {} } }