From 8b8cb23fb9222c82d13967471024ffae54af6d31 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:23:00 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EC=8B=A4=ED=96=89:=20?= =?UTF-8?q?=EB=A9=88=EC=B6=98=20=EC=B2=AB=20=EC=B0=BD=EC=9D=B4=EB=A9=B4=20?= =?UTF-8?q?=EC=95=BD=203=EC=B4=88=20=EC=95=88=EC=97=90=20"=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=97=86=EC=9D=8C"=20=EC=9D=84=20=EB=AC=BB?= =?UTF-8?q?=EB=8A=94=EB=8B=A4=20(=EC=98=88=EC=A0=84=20=EC=95=BD=2033?= =?UTF-8?q?=EC=B4=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 첫 창에 넘겨 본 결과를 셋으로: 받음 · 아직 준비 안 됨 (instance.json 없음 · 연결 안 됨) · 연결은 됐는데 답 없음 - 답 없음은 1초씩 3번이면 바로 unresponsive (관리자 결정 10/24). 첫 창이 정상이면 그 창에 넘기는 동작 · 막 뜨는 중이면 잠깐 다시 해 보는 동작은 그대로 - 약속 시험: 받기만 하고 답하지 않는 실제 소켓 - 5초 안에 unresponsive 확인: analyze 0 · 단위 1474 ×2 · player_test 17 ×2 · 통합 6/6 ×2 (a · c) · base10 214장 0px (b 회차는 시험 도중 C: 임시 폴더의 시험 폴더 jj_usechk_fixed_412 와 시험 휴지통 폴더가 통째로 없어져 412 23장 벗어남 - 이 고침과 무관, 원인 미확인) Co-Authored-By: Claude Opus 5.5 --- lib/platform/windows/single_instance.dart | 36 ++++++++++++++++++----- test/player_test.dart | 22 ++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/platform/windows/single_instance.dart b/lib/platform/windows/single_instance.dart index 62e0fbe..bb35f3b 100644 --- a/lib/platform/windows/single_instance.dart +++ b/lib/platform/windows/single_instance.dart @@ -18,6 +18,9 @@ Never endProcessNow() { exit(0); } +/// 첫 창에 넘겨 본 결과: 받음 · 아직 준비 안 됨 (파일 없음 · 연결 안 됨) · 연결은 됐는데 답이 없음 +enum _Forward { ok, notReady, silent } + /// [SingleInstance.claim] 의 결과 enum ClaimResult { /// 첫 실행 (요청을 받을 준비가 됨) @@ -72,10 +75,18 @@ class SingleInstance { continue; } if (!forward) return ClaimResult.running; - // 첫 창이 막 뜨는 중이면 instance.json 이 아직 없을 수 있다 - 잠깐 기다리며 몇 번 + // 첫 창이 막 뜨는 중이면 instance.json 이 아직 없을 수 있다 - 잠깐 기다리며 몇 번. + // 연결은 되는데 답이 없으면 (멈춘 첫 창) [silentWait] 씩 [silentTries] 번이면 묻는다 (관리자 10/24: 예전에는 약 33초 동안 반응 없음) + var silent = 0; for (var k = 0; k < 10; k++) { - if (await _forward(initial)) return ClaimResult.forwarded; - await Future.delayed(const Duration(milliseconds: 300)); + switch (await _forward(initial)) { + case _Forward.ok: + return ClaimResult.forwarded; + case _Forward.silent: + if (++silent >= silentTries) return ClaimResult.unresponsive; + case _Forward.notReady: + await Future.delayed(const Duration(milliseconds: 300)); + } } return ClaimResult.unresponsive; } @@ -111,18 +122,27 @@ class SingleInstance { if (_pending.isNotEmpty) _schedule(); } - Future _forward(LaunchRequest r) async { + /// 연결된 첫 창이 답하기를 기다리는 시간과 횟수 (넘으면 응답 없음 - 사용자에게 묻는다) + static const silentWait = Duration(seconds: 1); + static const silentTries = 3; + + Future<_Forward> _forward(LaunchRequest r) async { + Socket? s; try { final j = jsonDecode(_file.readAsStringSync()) as Map; final port = j['port'] as int, token = j['token'] as String; - final s = await Socket.connect(InternetAddress.loopbackIPv4, port, timeout: const Duration(seconds: 2)); + s = await Socket.connect(InternetAddress.loopbackIPv4, port, timeout: const Duration(seconds: 2)); s.write('$_magic $token ${jsonEncode(r.toArgs())}\n'); await s.flush(); - final reply = await utf8.decoder.bind(s).join().timeout(const Duration(seconds: 3)); + final reply = await utf8.decoder.bind(s).join().timeout(silentWait); await s.close(); - return reply.startsWith('OK'); + return reply.startsWith('OK') ? _Forward.ok : _Forward.notReady; + } on TimeoutException { + s?.destroy(); + return s == null ? _Forward.notReady : _Forward.silent; } catch (_) { - return false; + s?.destroy(); + return _Forward.notReady; } } diff --git a/test/player_test.dart b/test/player_test.dart index 0f7916b..b39e530 100644 --- a/test/player_test.dart +++ b/test/player_test.dart @@ -260,6 +260,28 @@ void main() { await holder.close(); }, skip: !Platform.isWindows); + test('첫 창이 연결은 받는데 답하지 않으면 5초 안에 unresponsive (관리자 10/24 - 예전 약 33초)', () async { + final holder = SingleInstance(dataDir: d1.path); + expect(await holder.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first); + // 멈춘 첫 창: 받기만 하고 답하지 않는 실제 소켓 + final mute = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final held = []; + mute.listen(held.add); + final info = File(p.join(d1.path, 'instance.json')); + info.writeAsStringSync(jsonEncode({...jsonDecode(info.readAsStringSync()) as Map, 'port': mute.port})); + final sw = Stopwatch()..start(); + expect(await SingleInstance(dataDir: d1.path).claim(const LaunchRequest(LaunchAction.play, [])), + ClaimResult.unresponsive); + sw.stop(); + expect(sw.elapsed, lessThan(const Duration(seconds: 5))); + expect(held, isNotEmpty, reason: '실제로 연결은 됐음'); + for (final s in held) { + s.destroy(); + } + await mute.close(); + await holder.close(); + }, skip: !Platform.isWindows); + test('잠금을 쥔 다른 프로세스가 죽으면 풀려서 다음 실행이 첫 실행', () async { // 다른 프로세스가 instance.lock 을 잠근 채 응답하지 않는다 (instance.json 없음) final lock = p.join(d1.path, 'instance.lock');