Files
dzc 726210edcb feat: 本地日志文件系统 + 修复双击打不开/向导完成无反应
用户实测两个问题(先装好驱动的机器):
1. 双击 App 图标不一定能打开——根因 single_instance:唤醒端口 38210 被
   残留占用(上次异常退出/TIME_WAIT)时,connect 无应答仍返回 false →
   main 直接 exit(0)。修复:端口被占但无应答判定为残留占用,照常启动
   (仅唤醒功能降级,FileLog 留痕);有应答才作为第二实例退出。
2. 向导第三步点"完成"没反应——根因 _finish 只有 finally 没有 catch:
   Autostart/UrlScheme(reg 三连)/FirstRun 任一抛异常被静默吞,Navigator.pop
   永不执行=对话框锁死。修复:每步独立 try-catch + 分步日志,成败都关闭
   向导,失败汇总 SnackBar 提示"详见运行日志"。

本地日志文件系统(用户要求:仔细记录、可发回分析、定时删除):
- lib/utils/file_log.dart:按天滚动 app-YYYYMMDD.log、单文件 5MB 轮转加
  序号、启动清理 7 天前旧文件(按文件名日期判定);全同步追加写(低频
  日志,保证异常退出前落盘);目录 mac ~/Library/Logs/zax-desktop-plugin/
  win %APPDATA%\zax-desktop-plugin\logs\(不引第三方依赖)
- AgentLog.write 挂文件输出(一处改动覆盖全部 22 个现有调用点);
  main.dart FileLog.init 提到最前,bootWarning/startError 全部落日志,
  启动横幅记录平台/exe 路径/参数
- pavo isolate 的 [pavo-diag] 诊断日志传日志路径(纯数据,isolate 安全)
  直接写文件——GUI 模式下 isolate stdout 本来就丢
- 插件页技术详情加"打开日志文件夹"按钮(Explorer/Finder 直达,用户打包
  app-*.log 发回分析)

19 个测试全过、analyze 零问题。
2026-09-22 15:09:24 +08:00

120 lines
4.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (_) {}
}
}