From e4b6e33a97dedf0590112bf4801f988564f52a0d Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:44:06 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=A5=B4=EB=8A=94=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20=EC=9A=94=EC=86=8C=20=EB=B3=B4=EC=A1=B4=20(225)=20?= =?UTF-8?q?=C2=B7=20Tor=20=EB=8C=80=EA=B8=B0=20=ED=91=9C=EC=8B=9C=20(226)?= =?UTF-8?q?=20=C2=B7=20=EB=A7=89=EC=9D=80=20=ED=8C=8C=EC=9D=BC=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=90=98=EB=8F=8C=EB=A6=BC=20(227)=20=C2=B7=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=8B=A4=EC=8B=9C=20=EC=9E=A1=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 225 (기기 확인 a4): Tor 를 모르는 판을 거치면 구성 요소 'tor' 가 지워지던 것. 설정을 읽을 때 components · navOrder 의 모르는 id 를 버리지 않고 그대로 둔다 (40 과 같은 원칙). 화면 · 버튼은 아는 것만. 화면 순서를 바꿔 저장할 때도 모르는 id 를 남긴다. - 226: Tor 화면을 떠난 뒤 tor 를 켜 두는 동안 "Tor 대기 중 (n분 뒤 끔)" 을 환경 설정 > Tor 와 일반 브라우저 띠에 [지금 끄기] 와 함께. - 227: Tor 화면에서 막은 파일 링크 뒤 주소 칸을 마지막으로 보인 페이지 주소로 되돌린다. - 포트 (관리자 지적): Windows 는 TCP · UDP 마다 막아 둔 포트 범위 (Hyper-V · WSL) 가 있어 번호를 정해 bind 하면 10013 이 날 수 있다. 시험의 가짜 DNS 는 TCP 를 OS 가 고른 번호로 먼저 열고 그 번호로 UDP (안 되면 다시). aria2 는 넘긴 번호를 열지 못해 끝나면 새 번호로 다시 (2번). 앱 안 프록시 · Tor 다리 (같은 번호 → 실패하면 OS 가 고름), Tor (auto), Android 웹 뷰 문 (0) 은 이미 그렇다. - analysis_options: packages/whisper_ggml 도 분석에서 뺌 (자기 analysis_options 가 flutter_lints 를 찾지 못함). - 시험: components_test 1 · tor_test 2 (226 · 227). Co-Authored-By: Claude Opus 5.5 --- analysis_options.yaml | 1 + assets/l10n/en.json | 3 ++ assets/l10n/ja.json | 3 ++ assets/l10n/zh-Hans.json | 3 ++ lib/app/settings.dart | 7 ++-- lib/platform/windows/aria2_backend.dart | 25 +++++++++++--- lib/ui/browser_page.dart | 26 +++++++++++++++ lib/ui/component_settings.dart | 2 ++ lib/ui/network_settings.dart | 12 +++++++ lib/ui/tor_page.dart | 24 +++++++++++++- test/components_test.dart | 13 ++++++++ test/support/fake_dns.dart | 13 +++++--- test/tor_test.dart | 43 +++++++++++++++++++++++++ 13 files changed, 163 insertions(+), 12 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index cb00d3c..48ca3d7 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -18,6 +18,7 @@ analyzer: - packages/flutter_inappwebview_android/** - packages/ffmpeg_kit_flutter_new_min_gpl/** # 고친 외부 부품 (원본 코드 그대로) - packages/flutter_inappwebview_windows/** # 고친 외부 부품 (Tor 화면 다운로드 막기 · 프록시 인증, JJ_PATCH.md) + - packages/whisper_ggml/** # 고친 외부 부품 (자기 analysis_options 가 flutter_lints 를 찾지 못함) linter: # The lint rules applied to this project can be customized in the diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 536d1b7..266d9cc 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -246,6 +246,7 @@ "Tor 내려받기 시작: {0}": "Tor download started: {0}", "Tor 대기 ({0}분 뒤 끔)": "Tor idle (stops in {0} min)", "Tor 대기 중 ({0}분 뒤 끔)": "Tor waiting (stops in {0} min)", + "Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다": "Tor waiting (stops in {0} min) - this browser does not use Tor", "Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "In-app web browser that goes out through Tor (separate cookies and history, cleared when closed)", "Tor 를 받을 수 없습니다: {0}": "Cannot download Tor: {0}", "Tor 를 시작하는 중": "Starting Tor", @@ -274,6 +275,7 @@ "Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "Keep cookies, history and cache after closing the Tor screen", "Tor 화면을 떠나 페이지를 정리했습니다": "You left the Tor screen, so its pages were cleared", "Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "Keep Tor running after leaving the Tor screen", + "Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.": "Tor is kept running so you can use it right away when you return to the Tor screen.", "Tor 화면을 준비할 수 없습니다": "Cannot prepare the Tor screen", "Tor 화면을 준비할 수 없습니다: {0}": "Cannot prepare the Tor screen: {0}", "Tor 화면의 JavaScript 사용": "JavaScript on the Tor screen", @@ -1817,6 +1819,7 @@ "즐겨찾기 표시줄": "Bookmarks bar", "지금": "Current", "지금 {0}%": "Now {0}%", + "지금 끄기": "Stop now", "지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?", "지금 마스터 (또는 최상) 비밀번호": "Current master (or top) password", "지금 맞추기": "Sync now", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 72350a3..4ed5d8d 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -246,6 +246,7 @@ "Tor 내려받기 시작: {0}": "Tor のダウンロード開始: {0}", "Tor 대기 ({0}분 뒤 끔)": "Tor 待機 ({0} 分後に終了)", "Tor 대기 중 ({0}분 뒤 끔)": "Tor 待機中 ({0} 分後に終了)", + "Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다": "Tor 待機中 ({0} 分後に終了) - このブラウザーは Tor を使いません", "Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "Tor 経由で通信するアプリ内 Web ブラウザー (Cookie・履歴は別、閉じると消去)", "Tor 를 받을 수 없습니다: {0}": "Tor をダウンロードできません: {0}", "Tor 를 시작하는 중": "Tor を起動中", @@ -274,6 +275,7 @@ "Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "Tor 画面を閉じても Cookie・履歴・キャッシュを残す", "Tor 화면을 떠나 페이지를 정리했습니다": "Tor 画面を離れたため、ページを片付けました", "Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "Tor 画面を離れた後に Tor を動かしておく時間", + "Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.": "Tor 画面に戻ったときすぐ使えるよう、Tor を動かしたままにしています。", "Tor 화면을 준비할 수 없습니다": "Tor 画面を準備できません", "Tor 화면을 준비할 수 없습니다: {0}": "Tor 画面を準備できません: {0}", "Tor 화면의 JavaScript 사용": "Tor 画面で JavaScript を使用", @@ -1817,6 +1819,7 @@ "즐겨찾기 표시줄": "ブックマークバー", "지금": "現在", "지금 {0}%": "現在 {0}%", + "지금 끄기": "今すぐ終了", "지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?", "지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード", "지금 맞추기": "今すぐ同期", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index db1ebf0..8aa89f4 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -246,6 +246,7 @@ "Tor 내려받기 시작: {0}": "开始下载 Tor: {0}", "Tor 대기 ({0}분 뒤 끔)": "Tor 待机({0} 分钟后关闭)", "Tor 대기 중 ({0}분 뒤 끔)": "Tor 待机中({0} 分钟后关闭)", + "Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다": "Tor 待机中({0} 分钟后关闭)- 此浏览器不使用 Tor", "Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "经由 Tor 访问的应用内网页浏览器 (Cookie · 记录分开,关闭时清除)", "Tor 를 받을 수 없습니다: {0}": "无法下载 Tor: {0}", "Tor 를 시작하는 중": "正在启动 Tor", @@ -274,6 +275,7 @@ "Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "关闭 Tor 画面后仍保留 Cookie · 记录 · 缓存", "Tor 화면을 떠나 페이지를 정리했습니다": "已离开 Tor 页面,页面已清理", "Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "离开 Tor 页面后 Tor 保持运行的时间", + "Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.": "Tor 保持运行,以便返回 Tor 页面时可立即使用。", "Tor 화면을 준비할 수 없습니다": "无法准备 Tor 画面", "Tor 화면을 준비할 수 없습니다: {0}": "无法准备 Tor 画面: {0}", "Tor 화면의 JavaScript 사용": "Tor 画面使用 JavaScript", @@ -1817,6 +1819,7 @@ "즐겨찾기 표시줄": "书签栏", "지금": "当前", "지금 {0}%": "当前 {0}%", + "지금 끄기": "立即关闭", "지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?", "지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)", "지금 맞추기": "立即同步", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 995da5a..510415c 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -885,13 +885,16 @@ class AppSettings { ..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x'] ..components = j['components'] is List ? [ - for (final x in j['components'] as List) if (AppComponent.byId('$x') != null) '$x', + // 225: 이 판이 모르는 구성 요소 (새 판의 Tor 등) 도 지우지 않고 그대로 둔다 - 예전 판을 거쳐 다시 올려도 켠 것이 남게 (40 과 같은 원칙). + // 화면 · 버튼은 아는 것만 쓴다 (AppComponent.pages · contains) + for (final x in j['components'] as List) if (x is String && x.isNotEmpty) x, // 123: 새 화면 "AI 그림" 은 예전 설정에도 한 번 켜서 넣는다 (끄기: 환경 설정 > 컴포넌트) if (j['aiImageV1'] != true && !(j['components'] as List).contains('aiimage')) 'aiimage', ] : AppComponent.defaultInstalled ..navOrder = j['navOrder'] is List - ? [for (final x in j['navOrder'] as List) if (AppComponent.defaultOrder.contains('$x')) '$x'] + // 225: 모르는 화면 id 도 순서에 그대로 (그 판으로 돌아가면 사용자가 정한 순서대로) + ? [for (final x in j['navOrder'] as List) if (x is String && x.isNotEmpty) x] : AppComponent.defaultOrder ..swipeNav = j['swipeNav'] as bool? ?? true ..imageExts = j['imageExts'] is List diff --git a/lib/platform/windows/aria2_backend.dart b/lib/platform/windows/aria2_backend.dart index e3baf02..a0f73da 100644 --- a/lib/platform/windows/aria2_backend.dart +++ b/lib/platform/windows/aria2_backend.dart @@ -44,7 +44,18 @@ class Aria2Backend implements DownloadBackend { Future _ensureDaemon() async { if (_daemon != null) return; - // 빈 포트 고르기 + // 번호를 넘겨 aria2 가 직접 여는 방식이라, 닫고 넘기는 사이에 다른 프로그램이 가져가면 aria2 가 끝난다 - 그때는 새 번호로 몇 번 다시 + for (var attempt = 0;; attempt++) { + try { + return await _startDaemon(); + } on SocketException { + if (attempt >= 2) rethrow; + } + } + } + + Future _startDaemon() async { + // 빈 포트 고르기 (OS 가 고른 번호 - Windows 가 막아 둔 범위에는 걸리지 않는다) final s = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); _port = s.port; await s.close(); @@ -63,9 +74,13 @@ class Aria2Backend implements DownloadBackend { ], environment: environment.isEmpty ? null : environment); _daemon!.stdout.drain(); _daemon!.stderr.drain(); - unawaited(_daemon!.exitCode.then((_) => _daemon = null)); - // RPC 준비 대기 - for (var i = 0; i < 50; i++) { + // 이 프로세스가 끝나면 (다시 띄운 새 것은 건드리지 않게) + final proc = _daemon!; + unawaited(proc.exitCode.then((_) { + if (identical(_daemon, proc)) _daemon = null; + })); + // RPC 준비 대기 (aria2 가 그 사이 끝났으면 - 포트를 열지 못함 등 - 기다리지 않는다) + for (var i = 0; i < 50 && _daemon != null; i++) { try { await _call('aria2.getVersion', []); return; @@ -73,6 +88,8 @@ class Aria2Backend implements DownloadBackend { await Future.delayed(const Duration(milliseconds: 100)); } } + _daemon?.kill(); + _daemon = null; throw SocketException(tr('aria2 RPC 에 연결할 수 없습니다.')); } diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index af8232e..4446b9a 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -12,6 +12,7 @@ import '../app/app_controller.dart'; import '../app/bookmarks_controller.dart'; import '../app/download_manager.dart'; import '../app/net_guard.dart'; +import '../app/tor_service.dart'; import '../app/web_proxy.dart'; import '../core/bookmarks.dart'; import '../core/local_proxy.dart' show ProxyAuth; @@ -321,6 +322,26 @@ class _BrowserPageState extends State with RouteAware { ), ); + Widget _torIdleBanner(DateTime at) => Material( + color: JjColors.panel, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 2, 4, 2), + child: Row(children: [ + const Icon(Icons.timer_outlined, size: 16, color: JjColors.textDim), + const SizedBox(width: 8), + Expanded( + child: Text( + trf('Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다', [(at.difference(DateTime.now()).inSeconds / 60).ceil().clamp(0, 60)]), + style: const TextStyle(fontSize: 12, color: JjColors.textDim), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + TextButton(onPressed: () => TorService.instance.stop(), child: Text(tr('지금 끄기'))), + ]), + ), + ); + Widget _torBanner() => Material( color: const Color(0xFF9C6ADE).withValues(alpha: 0.18), child: Padding( @@ -552,6 +573,11 @@ class _BrowserPageState extends State with RouteAware { valueListenable: WebProxy.torOpen, builder: (_, on, _) => on ? _torBanner() : const SizedBox(), ), + // 226: Tor 화면을 떠난 뒤 tor 를 잠시 켜 두는 중 (이 브라우저는 Tor 를 쓰지 않음) - 언제 끄는지 · 지금 끄기 + ValueListenableBuilder( + valueListenable: TorService.instance.idleStopAt, + builder: (_, at, _) => at == null || WebProxy.torOpen.value ? const SizedBox() : _torIdleBanner(at), + ), Expanded(child: Row(children: [ if (_panel) SizedBox( diff --git a/lib/ui/component_settings.dart b/lib/ui/component_settings.dart index ef9050b..f762124 100644 --- a/lib/ui/component_settings.dart +++ b/lib/ui/component_settings.dart @@ -183,6 +183,8 @@ class ComponentSettings extends StatelessWidget { c.updateSettings((v) => v.navOrder = [ ...ids, ...AppComponent.defaultOrder.where((x) => !ids.contains(x) && x != AppComponent.tor.id), + // 225: 이 판이 모르는 화면 id 는 지우지 않고 뒤에 그대로 + ...v.navOrder.where((x) => AppComponent.byId(x) == null), ]); } diff --git a/lib/ui/network_settings.dart b/lib/ui/network_settings.dart index 68e3017..7ab8418 100644 --- a/lib/ui/network_settings.dart +++ b/lib/ui/network_settings.dart @@ -148,6 +148,18 @@ class TorSettings extends StatelessWidget { Widget build(BuildContext context) { final s = c.settings; return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Tor 화면을 떠난 뒤 잠시 켜 두는 중이면 보인다 (언제 끄는지 · 지금 끄기) + ValueListenableBuilder( + valueListenable: TorService.instance.idleStopAt, + builder: (context, at, _) => at == null + ? const SizedBox.shrink() + : SettingTile( + leading: const Icon(Icons.timer_outlined), + title: Text(trf('Tor 대기 중 ({0}분 뒤 끔)', [(at.difference(DateTime.now()).inSeconds / 60).ceil().clamp(0, 60)])), + subtitle: Text(tr('Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.')), + trailing: OutlinedButton(onPressed: () => TorService.instance.stop(), child: Text(tr('지금 끄기'))), + ), + ), SwitchListTile( value: s.torKeepData, onChanged: (v) => c.updateSettings((x) => x.torKeepData = v), diff --git a/lib/ui/tor_page.dart b/lib/ui/tor_page.dart index 4715f6d..006f878 100644 --- a/lib/ui/tor_page.dart +++ b/lib/ui/tor_page.dart @@ -34,6 +34,9 @@ abstract class TorNav { /// 웹 뷰 → 화면으로 알리는 통로 class TorViewHost { final void Function(String url) onUrl; + + /// 페이지가 실제로 열림 (읽기 끝 · 기록에 남음). 막은 파일 주소로 주소 칸이 바뀌었으면 이 주소로 되돌린다 + final void Function(String url)? onShown; final void Function(double progress) onProgress; final void Function(bool back, bool forward) onHistory; final void Function(TorNav nav) attach; @@ -41,7 +44,12 @@ class TorViewHost { /// 파일 받기를 막았다 (Tor 화면은 받지 않는다 - 관리자 결정 10/11) final void Function(String url) onDownloadBlocked; const TorViewHost( - {required this.onUrl, required this.onProgress, required this.onHistory, required this.attach, required this.onDownloadBlocked}); + {required this.onUrl, + required this.onProgress, + required this.onHistory, + required this.attach, + required this.onDownloadBlocked, + this.onShown}); } /// WebRTC 끄기 (모든 틀 · 페이지 스크립트보다 먼저): STUN 으로 진짜 주소가 새지 않게. @@ -230,11 +238,23 @@ class _TorPageState extends State with WebProxyVisibility { }, attach: (n) => _nav = TorPage.debugNav = n, onDownloadBlocked: (_) => _downloadBlocked(), + onShown: (u) { + if (u.isNotEmpty) _shownUrl = u; + }, ); + /// 지금 보이는 페이지의 주소 (막은 파일 링크는 열리지 않으므로 주소 칸을 이것으로 되돌린다) + String? _shownUrl; + /// 파일 링크를 누르면: 받지 않고 이유를 알린다 (아무 반응이 없지 않게) void _downloadBlocked() { if (!mounted) return; + // 주소 칸이 막은 파일 주소로 바뀌었으면 보이는 페이지 주소로 (페이지와 주소가 다르지 않게) + final shown = _shownUrl; + if (shown != null) { + setState(() => _url = shown); + if (!_addressFocus.hasFocus) _address.text = shown; + } final m = ScaffoldMessenger.maybeOf(context); m?.hideCurrentSnackBar(); m?.showSnackBar(SnackBar(content: Text(tr('Tor 화면에서는 파일을 받지 않습니다 (Tor 밖으로 나가지 않게)')))); @@ -671,11 +691,13 @@ class _TorWebViewState extends State<_TorWebView> { onLoadStart: (c, u) => host.onUrl(u?.toString() ?? ''), onLoadStop: (c, u) async { host.onUrl(u?.toString() ?? ''); + host.onShown?.call(u?.toString() ?? ''); host.onProgress(1); await history(c); }, onUpdateVisitedHistory: (c, u, _) async { host.onUrl(u?.toString() ?? ''); + host.onShown?.call(u?.toString() ?? ''); await history(c); }, onProgressChanged: (c, v) => host.onProgress(v / 100), diff --git a/test/components_test.dart b/test/components_test.dart index eba6650..84b19eb 100644 --- a/test/components_test.dart +++ b/test/components_test.dart @@ -34,6 +34,19 @@ void main() { expect(AppSettings.fromJson({}).components, AppComponent.defaultInstalled); }); + test('225: 이 판이 모르는 구성 요소 · 화면 id 는 지우지 않는다 (예전 판을 거쳐 다시 올려도 켠 것이 남게)', () { + final j = {'aiImageV1': true, 'components': ['mkv', 'browser', 'future_screen'], 'navOrder': ['browser', 'future_screen', 'mkv']}; + final s = AppSettings.fromJson(j); + expect(s.components, ['mkv', 'browser', 'future_screen']); + expect(s.navOrder, ['browser', 'future_screen', 'mkv']); + // 화면 · 버튼에는 아는 것만 + expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id], ['browser', 'mkv']); + // 저장했다 다시 읽어도 그대로 + final back = AppSettings.fromJson(s.toJson()); + expect(back.components, contains('future_screen')); + expect(back.navOrder, contains('future_screen')); + }); + testWidgets('위쪽 이동 버튼: 설치한 화면만 · 설정을 바꾸면 바로 · 하나뿐이면 그것만', (tester) async { tester.view.physicalSize = const Size(1500, 900); tester.view.devicePixelRatio = 1; diff --git a/test/support/fake_dns.dart b/test/support/fake_dns.dart index 421fbde..c0f1032 100644 --- a/test/support/fake_dns.dart +++ b/test/support/fake_dns.dart @@ -21,14 +21,17 @@ class FakeDns { if (!truncateUdp) { _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); } else { - // TCP 로 다시 묻는 시험: UDP 와 TCP 가 같은 포트 (Windows 는 막아 둔 포트가 있어 몇 번 시도) + // TCP 로 다시 묻는 시험: UDP 와 TCP 가 같은 포트. Windows 는 TCP · UDP 마다 막아 둔 포트 범위 (Hyper-V · WSL, + // netsh int ipv4 show excludedportrange) 가 달라, TCP 를 먼저 OS 가 고른 번호로 열고 그 번호로 UDP 를 연다. + // 그 번호의 UDP 가 막혀 있거나 (10013) 쓰는 중이면 다른 번호로 다시 for (var i = 0;; i++) { - _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); + final tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); try { - _tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, _udp.port); + _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, tcp.port); + _tcp = tcp; break; - } catch (_) { - _udp.close(); + } on SocketException { + await tcp.close(); if (i > 50) rethrow; } } diff --git a/test/tor_test.dart b/test/tor_test.dart index 218fc7b..ecba8d9 100644 --- a/test/tor_test.dart +++ b/test/tor_test.dart @@ -18,6 +18,7 @@ import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/app_actions.dart'; import 'package:jj_mkvmaker/ui/tor_page.dart'; +import 'package:jj_mkvmaker/ui/network_settings.dart'; void main() { test('tor 알림 줄 읽기 · torrc', () { @@ -319,6 +320,48 @@ void main() { expect(AppSettings.fromJson({'torIdleMinutes': 'x'}).torIdleMinutes, 2); }); + testWidgets('227: 막은 파일 주소는 주소 칸에 남지 않고 보이던 페이지 주소로', (t) async { + final loads = []; + final hosts = []; + await open(t, + startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100), + applyProxy: () async => true, + loads: loads, + hosts: hosts); + await t.pump(); + await t.pump(); + final h = hosts.last; + h.onUrl('https://check.torproject.org/'); + h.onShown?.call('https://check.torproject.org/'); + // 파일 링크: 읽기 시작에 주소 칸이 파일 주소로 바뀐 뒤 막힘 + h.onUrl('https://dist.torproject.org/x.tar.gz.asc'); + await t.pump(); + expect(find.text('https://dist.torproject.org/x.tar.gz.asc'), findsOneWidget); + h.onDownloadBlocked('https://dist.torproject.org/x.tar.gz.asc'); + await t.pump(); + expect(find.text('https://dist.torproject.org/x.tar.gz.asc'), findsNothing); + expect(find.text('https://check.torproject.org/'), findsOneWidget); + }); + + testWidgets('226: 떠난 뒤 tor 를 켜 두는 동안 환경 설정 > Tor 에 "Tor 대기 중 (n분 뒤 끔)" · [지금 끄기]', (t) async { + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final s = TorService.instance; + addTearDown(() async { + s.cancelIdleStop(); + s.debugRunning = false; + }); + await t.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: TorSettings(c: c))))); + expect(find.textContaining('Tor 대기 중'), findsNothing); + s.debugRunning = true; + await s.stopAfter(const Duration(minutes: 2)); + await t.pump(); + expect(find.text('Tor 대기 중 (2분 뒤 끔)'), findsOneWidget); + await t.tap(find.text('지금 끄기')); + await t.pump(); + expect(s.running, isFalse); + expect(find.textContaining('Tor 대기 중'), findsNothing); + }); + testWidgets('파일 링크를 누르면 받지 않고 이유를 알린다 (아무 반응 없음이 아니게)', (t) async { final loads = []; final hosts = [];