fix(client): 关窗口缩托盘不退出 + 单实例(一机一进程)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled

- 关窗口托盘也消失:根因是 AppDelegate.applicationShouldTerminateAfterLast
  WindowClosed 返回 true → 关窗口即终止 app。改 false:关窗口只隐藏到托盘
  (配合 window_manager preventClose+hide),app 留在托盘。
- 单实例:main 启动先 ensureSingleInstance()——loopback 47654 端口锁;已有实例
  时连过去唤起其窗口显示,本进程 exit(0)。保证一机仅一个 pangolin 进程,
  重复启动只会前置已有实例。

flutter analyze 0 error;测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-19 09:34:12 +08:00
parent 31443d0fe7
commit 3f62d54d23
3 changed files with 35 additions and 2 deletions
+27
View File
@@ -11,6 +11,33 @@ import 'package:window_manager/window_manager.dart';
bool get isDesktop =>
!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
// 单实例锁端口(仅 loopback)。第二个实例绑定失败 → 通知首实例显示窗口后退出。
const _singleInstancePort = 47654;
/// 保证一台机器只有一个 Pangolin 进程。返回 false 表示已有实例在跑
/// (本进程应退出,首实例已被唤起显示)。
Future<bool> ensureSingleInstance() async {
if (!isDesktop) return true;
try {
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, _singleInstancePort);
// 首实例:监听后续实例的"唤起"请求 → 显示并聚焦窗口。
server.listen((socket) {
socket.destroy();
windowManager.show();
windowManager.focus();
});
return true;
} on SocketException {
// 端口被占 = 已有实例 → 连过去让它显示,然后本进程退出。
try {
final s = await Socket.connect(InternetAddress.loopbackIPv4, _singleInstancePort,
timeout: const Duration(seconds: 1));
s.destroy();
} catch (_) {}
return false;
}
}
class TrayService with TrayListener, WindowListener {
TrayService._();
static final TrayService instance = TrayService._();