JJ_MKVMaker/lib/core/remote_idle.dart

120 lines
4.1 KiB
Dart

import 'dart:async';
import '../l10n/tr.dart';
/// 관리자 판단 (10/20): 원격 서버 (FTP · SFTP · SMB · WebDAV) 의 받기 · 올리기 자료 연결에서 조각 사이에 정한 시간 동안
/// 아무것도 오가지 않으면 실패한다. 예전에는 서버가 중간에 멈추면 복사 · 동기화가 영원히 멈춰 있었다 (명령의 답에만 시간 제한).
/// 정한 시간은 설정 "원격 서버 응답 기다리기" (처음 60초 · 30 · 60 · 120 · 300초) - 앱이 [limit] 에 넣는다
class RemoteIdle {
static const choices = [30, 60, 120, 300];
static const defaultSeconds = 60;
static Duration limit = const Duration(seconds: defaultSeconds);
}
/// 조각 사이에 정한 시간이 지나도 서버가 아무것도 주고받지 않음
class RemoteIdleTimeout implements Exception {
final int seconds;
const RemoteIdleTimeout(this.seconds);
@override
String toString() => trf('서버가 답하지 않습니다 ({0}초)', [seconds]);
}
/// 받기: 조각 사이가 [limit] (없으면 [RemoteIdle.limit]) 를 넘으면 [RemoteIdleTimeout] 으로 끝나는 스트림.
/// 그때 [onTimeout] 으로 그 연결을 끊고 원래 스트림의 구독도 끊는다 (늦게 오는 조각 · 오류를 받지 않음).
/// 받는 쪽이 멈춰 (pause) 있는 동안은 세지 않는다
Stream<T> idleRead<T>(Stream<T> s, {Duration? limit, void Function()? onTimeout}) {
final d = limit ?? RemoteIdle.limit;
late StreamController<T> out;
StreamSubscription<T>? sub;
Timer? t;
void arm() {
t?.cancel();
t = Timer(d, () {
final x = sub;
sub = null;
// 오류를 먼저 넣고 닫은 뒤에 연결을 끊는다 - 먼저 끊으면 원래 스트림의 "끝" 이 먼저 와서 정상으로 끝난 것처럼 보였다
if (!out.isClosed) {
out.addError(RemoteIdleTimeout(d.inSeconds));
unawaited(out.close());
}
onTimeout?.call();
unawaited(Future(() => x?.cancel()).catchError((_) {}));
});
}
out = StreamController<T>(
onListen: () {
arm();
sub = s.listen((v) {
if (out.isClosed) return;
arm();
out.add(v);
}, onError: (Object e, StackTrace st) {
if (out.isClosed) return;
arm();
out.addError(e, st);
}, onDone: () {
t?.cancel();
if (!out.isClosed) unawaited(out.close());
});
},
onPause: () {
t?.cancel();
sub?.pause();
},
onResume: () {
sub?.resume();
arm();
},
onCancel: () {
t?.cancel();
final x = sub;
sub = null;
return x?.cancel();
},
);
return out.stream;
}
/// 올리기: [send] 가 [data] 에서 [limit] 동안 한 조각도 더 가져가지 않으면 (서버가 받지 않아 보내기가 막힘) 또는 다 가져간 뒤
/// [limit] 안에 끝나지 않으면 (서버의 마지막 답이 없음) [RemoteIdleTimeout]. 그때 [onTimeout] 으로 그 연결을 끊는다
Future<T> idleWrite<T>(Stream<List<int>> data, Future<T> Function(Stream<List<int>> s) send,
{Duration? limit, void Function()? onTimeout}) {
final d = limit ?? RemoteIdle.limit;
final fail = Completer<T>();
Timer? t;
void arm() {
t?.cancel();
t = Timer(d, () {
if (fail.isCompleted) return;
fail.completeError(RemoteIdleTimeout(d.inSeconds));
onTimeout?.call();
});
}
arm();
final work = send(data.map((x) {
arm();
return x;
}));
work.then((v) {
t?.cancel();
if (!fail.isCompleted) fail.complete(v);
}, onError: (Object e, StackTrace st) {
t?.cancel();
if (!fail.isCompleted) fail.completeError(e, st);
});
return fail.future;
}
/// 한 번의 요청 (SMB 조각 읽기 · 쓰기, WebDAV 응답 기다리기) 에 같은 시간 제한
Future<T> idleCall<T>(Future<T> f, {Duration? limit, void Function()? onTimeout}) {
final d = limit ?? RemoteIdle.limit;
// 시간이 지나 연결을 끊은 뒤 원래 요청이 늦게 실패해도 (연결 닫힘 등) 처리되지 않은 오류로 남지 않게
f.ignore();
return f.timeout(d, onTimeout: () {
onTimeout?.call();
throw RemoteIdleTimeout(d.inSeconds);
});
}