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 acquire({void Function()? onWake}) async { try { final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, wakePort); server.listen((socket) { socket .cast>() .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; } } } }