diff --git a/lib/app/live_sync.dart b/lib/app/live_sync.dart index 4847e62..bbd6b65 100644 --- a/lib/app/live_sync.dart +++ b/lib/app/live_sync.dart @@ -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()); diff --git a/test/monitor_test.dart b/test/monitor_test.dart index 29431b5..83500b9 100644 --- a/test/monitor_test.dart +++ b/test/monitor_test.dart @@ -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.delayed(const Duration(milliseconds: 100)); + } + live.dispose(); + }); + // 모아서 3초 뒤 맞춘다 - 바쁠 때는 늦으므로 정해진 4초가 아니라 될 때까지 (최대 30초) + Future until(bool Function() done) async { + for (var i = 0; i < 120 && !done(); i++) { + await Future.delayed(const Duration(milliseconds: 250)); + } + } + expect(live.watching, isEmpty); await Future.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.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.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 {