用户实测两个问题(先装好驱动的机器): 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 零问题。
62 lines
2.5 KiB
Dart
62 lines
2.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import '../utils/file_log.dart';
|
|
|
|
/// 单实例 + 唤醒端口:应用只允许一个常驻实例。
|
|
///
|
|
/// zaxprint:// 唤起或用户再次双击图标时,OS 会启动第二个进程——第二实例连上
|
|
/// 唤醒端口发送 wake 后立即退出,常驻实例收到后把主窗口带到前台。
|
|
/// (比 app_links 的方案更跨平台一致:mac/win 都只依赖回环 socket。)
|
|
class SingleInstance {
|
|
static const int wakePort = 38210;
|
|
|
|
/// 尝试成为主实例。
|
|
///
|
|
/// 返回 true = 本实例是主实例(已开始监听唤醒,[onWake] 在收到唤醒时回调);
|
|
/// 返回 false = 已有常驻实例(已通知其显示窗口),调用方应当退出进程。
|
|
///
|
|
/// 唤醒端口被占用但无人应答(上次异常退出的残留/TIME_WAIT)时**照样返回
|
|
/// true 启动应用**——曾因误判为"已有实例"直接 exit(0),导致双击图标
|
|
/// 间歇性打不开;此时仅唤醒功能降级(FileLog 留痕),绝不阻断启动。
|
|
static Future<bool> acquire({void Function()? onWake}) async {
|
|
try {
|
|
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, wakePort);
|
|
server.listen((socket) {
|
|
socket
|
|
.cast<List<int>>()
|
|
.transform(utf8.decoder)
|
|
.listen((message) {
|
|
if (message.contains('wake')) {
|
|
onWake?.call();
|
|
}
|
|
}, onDone: () {
|
|
socket.destroy();
|
|
});
|
|
}, onError: (Object e) {
|
|
FileLog.write('唤醒监听异常(不影响服务):$e');
|
|
});
|
|
return true;
|
|
} on SocketException {
|
|
// 端口被占:可能是已有常驻实例,也可能是残留占用——先尝试唤醒握手
|
|
try {
|
|
final socket = await Socket.connect(
|
|
InternetAddress.loopbackIPv4,
|
|
wakePort,
|
|
timeout: const Duration(seconds: 2),
|
|
);
|
|
socket.add(utf8.encode('wake'));
|
|
await socket.flush();
|
|
await socket.close();
|
|
FileLog.write('唤醒端口有应答(已有常驻实例),本进程作为第二实例退出');
|
|
return false; // 真有实例在听且已收到唤醒
|
|
} catch (e) {
|
|
// 无应答:残留占用。放弃唤醒监听但照常启动(唤醒功能本次降级)
|
|
FileLog.write('唤醒端口被占但无应答(判定为残留占用,唤醒功能本次降级):$e');
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|