JJ_MKVMaker/lib/platform/windows/single_instance.dart

98 lines
3.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import '../../core/playlist.dart';
/// 프로그램을 하나만 실행하고, 나중에 실행된 것은 인수를 첫 번째 창에 넘기고 끝낸다.
///
/// 탐색기에서 파일 여러 개를 골라 "재생" 하면 파일마다 프로그램이 실행되므로,
/// 짧은 시간([gather]) 안에 들어온 요청을 하나로 모아 전달한다.
class SingleInstance {
static const _magic = 'JJMKVMAKER1';
final int port;
final Duration gather;
ServerSocket? _server;
final _pending = <LaunchAction, List<String>>{};
Timer? _timer;
void Function(LaunchRequest)? _onRequest;
SingleInstance({this.port = 47821, this.gather = const Duration(milliseconds: 700)});
/// 첫 번째 실행이면 true (요청을 받을 준비). 이미 실행 중이면 인수를 넘기고 false.
/// [waitForExit]: 재시작 - 기존 창이 닫힐 때까지 (최대 15초) 기다렸다가 첫 번째가 된다.
Future<bool> claim(LaunchRequest initial, {bool waitForExit = false}) async {
for (var i = 0;; i++) {
try {
_server = await ServerSocket.bind(InternetAddress.loopbackIPv4, port);
break;
} on SocketException {
if (waitForExit && i < 60) {
await Future<void>.delayed(const Duration(milliseconds: 250));
continue;
}
if (await _forward(initial)) return false;
return true; // 다른 프로그램이 포트를 쓰는 경우: 넘기지 못했으므로 그냥 실행
}
}
_server!.listen(_accept);
_add(initial);
return true;
}
/// 모인 요청을 처리할 함수 (앱 준비가 끝난 뒤 등록)
void listen(void Function(LaunchRequest) onRequest) {
_onRequest = onRequest;
if (_pending.isNotEmpty) _schedule();
}
Future<bool> _forward(LaunchRequest r) async {
try {
final s = await Socket.connect(InternetAddress.loopbackIPv4, port,
timeout: const Duration(seconds: 2));
s.write('$_magic ${jsonEncode(r.toArgs())}\n');
await s.flush();
final reply = await utf8.decoder.bind(s).join().timeout(const Duration(seconds: 3));
await s.close();
return reply.startsWith('OK');
} catch (_) {
return false;
}
}
void _accept(Socket s) {
utf8.decoder.bind(s).transform(const LineSplitter()).first.then((line) {
if (line.startsWith('$_magic ')) {
final args = (jsonDecode(line.substring(_magic.length + 1)) as List).cast<String>();
_add(LaunchRequest.parse(args));
s.write('OK');
}
return s.close();
}).catchError((_) => s.destroy());
}
void _add(LaunchRequest r) {
// 파일 없이 실행된 경우도 "창 보이기" 요청으로 전달
_pending.putIfAbsent(r.action, () => []).addAll(r.files);
_schedule();
}
void _schedule() {
if (_onRequest == null) return;
_timer?.cancel();
_timer = Timer(gather, () {
final items = Map.of(_pending);
_pending.clear();
for (final e in items.entries) {
_onRequest!(LaunchRequest(e.key, e.value.toSet().toList()));
}
});
}
Future<void> close() async {
_timer?.cancel();
await _server?.close();
}
}