用户实测两个问题(先装好驱动的机器): 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 零问题。
120 lines
4.3 KiB
Dart
120 lines
4.3 KiB
Dart
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 (_) {}
|
||
}
|
||
}
|