단일 실행: 멈춘 첫 창이면 약 3초 안에 "응답 없음" 을 묻는다 (예전 약 33초)

- 첫 창에 넘겨 본 결과를 셋으로: 받음 · 아직 준비 안 됨 (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 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-24 05:23:00 +09:00
parent eaaa096ea1
commit 8b8cb23fb9
2 changed files with 50 additions and 8 deletions

View File

@ -18,6 +18,9 @@ Never endProcessNow() {
exit(0); exit(0);
} }
/// 첫 창에 넘겨 본 결과: 받음 · 아직 준비 안 됨 (파일 없음 · 연결 안 됨) · 연결은 됐는데 답이 없음
enum _Forward { ok, notReady, silent }
/// [SingleInstance.claim] 의 결과 /// [SingleInstance.claim] 의 결과
enum ClaimResult { enum ClaimResult {
/// 첫 실행 (요청을 받을 준비가 됨) /// 첫 실행 (요청을 받을 준비가 됨)
@ -72,11 +75,19 @@ class SingleInstance {
continue; continue;
} }
if (!forward) return ClaimResult.running; if (!forward) return ClaimResult.running;
// 첫 창이 막 뜨는 중이면 instance.json 이 아직 없을 수 있다 - 잠깐 기다리며 몇 번 // 첫 창이 막 뜨는 중이면 instance.json 이 아직 없을 수 있다 - 잠깐 기다리며 몇 번.
// 연결은 되는데 답이 없으면 (멈춘 첫 창) [silentWait] 씩 [silentTries] 번이면 묻는다 (관리자 10/24: 예전에는 약 33초 동안 반응 없음)
var silent = 0;
for (var k = 0; k < 10; k++) { for (var k = 0; k < 10; k++) {
if (await _forward(initial)) return ClaimResult.forwarded; switch (await _forward(initial)) {
case _Forward.ok:
return ClaimResult.forwarded;
case _Forward.silent:
if (++silent >= silentTries) return ClaimResult.unresponsive;
case _Forward.notReady:
await Future<void>.delayed(const Duration(milliseconds: 300)); await Future<void>.delayed(const Duration(milliseconds: 300));
} }
}
return ClaimResult.unresponsive; return ClaimResult.unresponsive;
} }
await _listen(); await _listen();
@ -111,18 +122,27 @@ class SingleInstance {
if (_pending.isNotEmpty) _schedule(); if (_pending.isNotEmpty) _schedule();
} }
Future<bool> _forward(LaunchRequest r) async { /// 연결된 첫 창이 답하기를 기다리는 시간과 횟수 (넘으면 응답 없음 - 사용자에게 묻는다)
static const silentWait = Duration(seconds: 1);
static const silentTries = 3;
Future<_Forward> _forward(LaunchRequest r) async {
Socket? s;
try { try {
final j = jsonDecode(_file.readAsStringSync()) as Map; final j = jsonDecode(_file.readAsStringSync()) as Map;
final port = j['port'] as int, token = j['token'] as String; 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'); s.write('$_magic $token ${jsonEncode(r.toArgs())}\n');
await s.flush(); 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(); 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 (_) { } catch (_) {
return false; s?.destroy();
return _Forward.notReady;
} }
} }

View File

@ -260,6 +260,28 @@ void main() {
await holder.close(); await holder.close();
}, skip: !Platform.isWindows); }, 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 = <Socket>[];
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 { test('잠금을 쥔 다른 프로세스가 죽으면 풀려서 다음 실행이 첫 실행', () async {
// 다른 프로세스가 instance.lock 을 잠근 채 응답하지 않는다 (instance.json 없음) // 다른 프로세스가 instance.lock 을 잠근 채 응답하지 않는다 (instance.json 없음)
final lock = p.join(d1.path, 'instance.lock'); final lock = p.join(d1.path, 'instance.lock');