단일 실행: 멈춘 첫 창이면 약 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
parent
eaaa096ea1
commit
8b8cb23fb9
|
|
@ -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<void>.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<void>.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
return ClaimResult.unresponsive;
|
||||
}
|
||||
|
|
@ -111,18 +122,27 @@ class SingleInstance {
|
|||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = <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 {
|
||||
// 다른 프로세스가 instance.lock 을 잠근 채 응답하지 않는다 (instance.json 없음)
|
||||
final lock = p.join(d1.path, 'instance.lock');
|
||||
|
|
|
|||
Loading…
Reference in New Issue