실시간 동기화: dispose 뒤에 맞추기가 끝나도 알리지 않음

- 버그 (관리자 전체 시험에서 발견): 앱을 끄거나 화면을 닫는 중에 맞추기가 끝나면 used after disposed 오류
- 시험은 정해진 4초가 아니라 맞추기가 끝날 때까지 기다리고, 끝난 뒤 지움 (흔들림)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
RyuJongJin 2026-10-12 00:36:34 +09:00
parent 037ddf502a
commit 3a9f09a6eb
2 changed files with 41 additions and 3 deletions

View File

@ -94,6 +94,7 @@ class LiveSync extends ChangeNotifier {
@override
void dispose() {
_disposed = true;
c.removeListener(_apply);
_clock?.cancel();
_stopAll();
@ -101,6 +102,15 @@ class LiveSync extends ChangeNotifier {
super.dispose();
}
/// 맞추기 (rsync · robocopy 프로세스) 는 dispose 뒤에도 끝까지 돌 수 있다 - 그때 끝나며 알리지 않게
/// (앱을 끄거나 화면을 닫는 중에 맞추기가 끝나면 "used after being disposed" 오류가 났다)
bool _disposed = false;
@override
void notifyListeners() {
if (!_disposed) super.notifyListeners();
}
/// 지금 동작 시간인지 (일정이 없으면 늘)
static bool activeNow(LiveSyncPair x, [DateTime? now]) => scheduleActive(x.schedule, now ?? DateTime.now());

View File

@ -207,22 +207,50 @@ void main() {
..liveSyncPairs = [pa, pb]
..liveSyncOnStart = 'off';
final live = LiveSync(c)..start(hold: c.settings.liveSyncOnStart != 'auto');
addTearDown(live.dispose);
// 맞추기 (rsync 프로세스) 가 끝난 뒤 닫고 지운다 - 도는 중에 지우면 "다른 프로세스가 사용 중" (전체 시험이 무거울 때)
addTearDown(() async {
for (var i = 0; i < 200 && live.anyRunning; i++) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
live.dispose();
});
// 모아서 3초 뒤 맞춘다 - 바쁠 때는 늦으므로 정해진 4초가 아니라 될 때까지 (최대 30초)
Future<void> until(bool Function() done) async {
for (var i = 0; i < 120 && !done(); i++) {
await Future<void>.delayed(const Duration(milliseconds: 250));
}
}
expect(live.watching, isEmpty);
await Future<void>.delayed(const Duration(seconds: 4));
expect(Directory(pa.target).existsSync(), isFalse); // 시작 안 함
live.runOnly([pa]);
expect(live.watching.map((x) => x.source), [a.path]);
expect(live.isPaused(pb), isTrue);
await Future<void>.delayed(const Duration(seconds: 4));
await until(() => File(p.join(pa.target, '1.txt')).existsSync() && !live.anyRunning);
expect(File(p.join(pa.target, '1.txt')).existsSync(), isTrue);
expect(Directory(pb.target).existsSync(), isFalse);
live.resumeAll();
await Future<void>.delayed(const Duration(seconds: 4));
await until(() => File(p.join(pb.target, '2.txt')).existsSync() && !live.anyRunning);
expect(File(p.join(pb.target, '2.txt')).existsSync(), isTrue);
live.pause(pa);
expect(live.watching.map((x) => x.source), [b.path]);
});
test('dispose 뒤에 맞추기가 끝나도 알리지 않는다 (앱을 끄는 중 - used after disposed 오류 없음)', () async {
final tmp = Directory.systemTemp.createTempSync('jj_disp_');
addTearDown(() => tmp.deleteSync(recursive: true));
final a = Directory(p.join(tmp.path, 'a'))..createSync();
File(p.join(a.path, '1.txt')).writeAsStringSync('1');
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final pair = LiveSyncPair(a.path, p.join(tmp.path, 't'), method: 'builtin');
c.settings.liveSyncPairs = [pair];
final live = LiveSync(c);
final running = live.syncNow(pair);
live.dispose();
await running; // 예전에는 여기서 notifyListeners 가 FlutterError
expect(File(p.join(tmp.path, 't', '1.txt')).existsSync(), isTrue, reason: '맞추기 자체는 끝까지');
});
});
testWidgets('모니터링 화면: 복사 목록 · 옵션 고치기 → 현재 유지 / 반영 후 실행, lsync 탭 · 일정', (tester) async {