250 웹 브라우저: DNS 서버에 닿지 않아 못 연 페이지는 이유 · [시스템 DNS 로 바꾸기] [다시 시도]
관리자 지시 (10/12, a4 자유 점검): 엔진 기본 오류 "net::ERR_TUNNEL_CONNECTION_FAILED" 만 보여 안내를 닫은 뒤에는 이유를 알 수 없었다. 앱 안 프록시가 기록한 실패 (NetGuard.dnsUnreachableFor: 이 페이지를 읽기 시작한 뒤 그 이름이 DNS 서버에 닿지 않음 · 아직 다시 답하지 않음) 로 판단해, 읽기가 끝나면 웹 뷰 위에 앱이 그린다. Edge · Chrome 엔진 · Android 모두 같은 길 (엔진마다 다른 오류 콜백에 기대지 않음). 새로 읽기 시작하면 걷힌다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
ba39f4bc92
commit
6c61b8b82d
|
|
@ -153,6 +153,7 @@
|
|||
"DNS 서버": "DNS server",
|
||||
"DNS 서버 ({0}) 에 닿지 않습니다: {1}": "Cannot reach the DNS server ({0}): {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 {1}": "Couldn't reach the DNS server ({0}), so {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 {1} 을(를) 열 수 없습니다.": "Can't open {1} because the DNS server ({0}) can't be reached.",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "Couldn't reach the DNS server ({0}), so the address couldn't be found. Check the internet connection or the server address in Settings > Network · DNS.",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다: {1}": "Could not look up the address because the DNS server ({0}) is unreachable: {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않음 ({1}부터)": "Can't reach DNS server ({0}) (since {1})",
|
||||
|
|
@ -1691,6 +1692,7 @@
|
|||
"인증서 확인 안 함": "Don't verify certificate",
|
||||
"인코딩": "Encoding",
|
||||
"인터넷 연결을 확인한 뒤 환경 설정 > 필수 프로그램 점검에서 다시 시도하세요.\n\n{0}": "Check your internet connection, then try again in Settings > Check required programs.\n\n{0}",
|
||||
"인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "Check your internet connection or the server address in Settings > Network · DNS.",
|
||||
"인터넷 자막 (OpenSubtitles.com)": "Internet subtitles (OpenSubtitles.com)",
|
||||
"인터넷 자막 찾기": "Find subtitles online",
|
||||
"인터넷 자막 찾기{0}": "Find subtitles online{0}",
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@
|
|||
"DNS 서버": "DNS サーバー",
|
||||
"DNS 서버 ({0}) 에 닿지 않습니다: {1}": "DNS サーバー ({0}) に届きません: {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 {1}": "DNS サーバー ({0}) に接続できないため、{1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 {1} 을(를) 열 수 없습니다.": "DNS サーバー ({0}) に接続できないため、{1} を開けません。",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "DNS サーバー ({0}) に届かず、アドレスを見つけられませんでした。インターネット接続か、環境設定 > ネットワーク · DNS のサーバーアドレスを確認してください。",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다: {1}": "DNS サーバー ({0}) に届かず、アドレスを解決できませんでした: {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않음 ({1}부터)": "DNS サーバー ({0}) に接続できません ({1} から)",
|
||||
|
|
@ -1691,6 +1692,7 @@
|
|||
"인증서 확인 안 함": "証明書を検証しない",
|
||||
"인코딩": "エンコード",
|
||||
"인터넷 연결을 확인한 뒤 환경 설정 > 필수 프로그램 점검에서 다시 시도하세요.\n\n{0}": "インターネット接続を確認してから、環境設定 > 必須プログラムの点検 でもう一度試してください。\n\n{0}",
|
||||
"인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "インターネット接続、または 設定 > ネットワーク · DNS のサーバーアドレスを確認してください。",
|
||||
"인터넷 자막 (OpenSubtitles.com)": "インターネット字幕(OpenSubtitles.com)",
|
||||
"인터넷 자막 찾기": "インターネットで字幕を探す",
|
||||
"인터넷 자막 찾기{0}": "インターネットで字幕を探す{0}",
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@
|
|||
"DNS 서버": "DNS 服务器",
|
||||
"DNS 서버 ({0}) 에 닿지 않습니다: {1}": "无法连接 DNS 服务器 ({0}): {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 {1}": "无法连接 DNS 服务器 ({0}),因此{1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 {1} 을(를) 열 수 없습니다.": "无法连接 DNS 服务器 ({0}),因此无法打开 {1}。",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "无法连接 DNS 服务器({0}),未能找到地址。请检查网络连接,或检查 设置 > 网络 · DNS 中的服务器地址。",
|
||||
"DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다: {1}": "无法连接 DNS 服务器 ({0}),未能解析地址: {1}",
|
||||
"DNS 서버 ({0}) 에 닿지 않음 ({1}부터)": "无法连接 DNS 服务器 ({0}) (自 {1} 起)",
|
||||
|
|
@ -1691,6 +1692,7 @@
|
|||
"인증서 확인 안 함": "不验证证书",
|
||||
"인코딩": "编码",
|
||||
"인터넷 연결을 확인한 뒤 환경 설정 > 필수 프로그램 점검에서 다시 시도하세요.\n\n{0}": "请检查网络连接后,在 设置 > 检查必需程序 中重试。\n\n{0}",
|
||||
"인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "请检查网络连接或 设置 > 网络 · DNS 中的服务器地址。",
|
||||
"인터넷 자막 (OpenSubtitles.com)": "网络字幕(OpenSubtitles.com)",
|
||||
"인터넷 자막 찾기": "在线查找字幕",
|
||||
"인터넷 자막 찾기{0}": "在线查找字幕{0}",
|
||||
|
|
|
|||
|
|
@ -94,6 +94,18 @@ class NetGuard {
|
|||
if (failure.value != null) failure.value = null;
|
||||
}
|
||||
|
||||
/// 250: [host] 를 [since] 뒤에 DNS 서버에 닿지 않아 잇지 못했으면 그 서버 (아니면 null).
|
||||
/// 웹 브라우저가 엔진의 기본 오류 화면 ("net::ERR_TUNNEL_CONNECTION_FAILED") 대신 이유와 할 일을 보이게
|
||||
String? dnsUnreachableFor(String host, DateTime since) {
|
||||
// 그 뒤 서버가 다시 답했으면 (failure 가 지워짐) 지난 실패로 막지 않는다
|
||||
if (failure.value == null) return null;
|
||||
final last = _lastError[host.toLowerCase()];
|
||||
if (last == null || last.$2.isBefore(since)) return null;
|
||||
final e = last.$1;
|
||||
if (e is DnsException && e.unreachable) return e.server.isEmpty ? config().label : e.server;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 247: 앱의 HTTP 요청이 DNS 강제 프록시에서 실패했으면 그 까닭을 사용자 글로 ("DNS 서버 (…) 에 닿지 않아 …").
|
||||
/// 프록시 실패가 아니면 null (부르는 쪽이 원래 글을 쓴다). [what] 은 무엇을 못 했는지 ("새 버전을 확인하지 못했습니다").
|
||||
String? explain(Object e, String what) {
|
||||
|
|
|
|||
|
|
@ -155,6 +155,66 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
late String _url = widget.initialUrl ?? BrowserPage._lastUrl ?? widget.c.settings.homeUrl;
|
||||
String _title = '';
|
||||
double _progress = 1;
|
||||
|
||||
/// 250: 이 페이지를 DNS 서버에 닿지 않아 열지 못함 (주소 이름 · DNS 서버). 엔진의 오류 화면 위에 이유와 할 일을 보인다
|
||||
(String, String)? _dnsBlocked;
|
||||
DateTime _loadStartAt = DateTime.now();
|
||||
|
||||
void _checkDnsBlocked() {
|
||||
final host = Uri.tryParse(_url)?.host ?? '';
|
||||
final server = host.isEmpty ? null : NetGuard.instance.dnsUnreachableFor(host, _loadStartAt);
|
||||
final next = server == null ? null : (host, server);
|
||||
if (next != _dnsBlocked) setState(() => _dnsBlocked = next);
|
||||
}
|
||||
|
||||
/// 250: DNS 실패면 웹 뷰 위에 덮는다 ([시스템 DNS 로 바꾸기] [다시 시도])
|
||||
Widget _withDnsBlock(Widget view) {
|
||||
final b = _dnsBlocked;
|
||||
if (b == null) return view;
|
||||
void retry() {
|
||||
setState(() => _dnsBlocked = null);
|
||||
_nav?.reload();
|
||||
}
|
||||
|
||||
return Stack(children: [
|
||||
Positioned.fill(child: view),
|
||||
Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: JjColors.bg,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.dns_outlined, size: 40, color: Colors.orange),
|
||||
const SizedBox(height: 12),
|
||||
Text(trf('DNS 서버 ({0}) 에 닿지 않아 {1} 을(를) 열 수 없습니다.', [b.$2, b.$1]),
|
||||
textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 8),
|
||||
Text(tr('인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.'),
|
||||
textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: [
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
await widget.c.updateSettings((x) => x.dnsMode = 'system');
|
||||
// 새 DNS 설정이 프록시에 걸린 뒤 다시 읽는다
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted) retry();
|
||||
},
|
||||
child: Text(tr('시스템 DNS 로 바꾸기')),
|
||||
),
|
||||
OutlinedButton(onPressed: retry, child: Text(tr('다시 시도'))),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
bool _hasVideo = false, _canBack = false, _canFwd = false, _panel = false;
|
||||
WebNav? _nav;
|
||||
|
||||
|
|
@ -178,8 +238,16 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
onTitle: (t) => mounted ? setState(() => _title = t) : null,
|
||||
onProgress: (p) {
|
||||
if (!mounted) return;
|
||||
// 250: 새로 읽기 시작 - 지난 DNS 실패 화면을 걷고 시작 시각을 적어 둔다
|
||||
if (p < 1 && _progress >= 1) {
|
||||
_loadStartAt = DateTime.now();
|
||||
_dnsBlocked = null;
|
||||
}
|
||||
setState(() => _progress = p);
|
||||
if (p >= 1) _translateNow(); // 다 읽었다: 바로 번역 (2초를 기다리지 않고)
|
||||
if (p >= 1) {
|
||||
_checkDnsBlocked();
|
||||
_translateNow(); // 다 읽었다: 바로 번역 (2초를 기다리지 않고)
|
||||
}
|
||||
},
|
||||
onVideo: (v) => mounted ? setState(() => _hasVideo = v) : null,
|
||||
onHistory: (b, f) {
|
||||
|
|
@ -616,7 +684,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
child: _progress < 1 ? LinearProgressIndicator(value: _progress) : null,
|
||||
),
|
||||
Expanded(
|
||||
child: widget.viewBuilder?.call(_host, _url) ??
|
||||
child: _withDnsBlock(widget.viewBuilder?.call(_host, _url) ??
|
||||
// 환경 설정에서 Chrome 을 고르고 엔진이 이번 실행에 준비되어 있으면 Chrome, 아니면 Edge
|
||||
(widget.c.settings.browserEngine == 'chrome' && CefRuntime.readyThisRun
|
||||
// 207: 관리자 권한이면 처음 값으로 Chrome 엔진을 시작하지 않는다 (보안) - 이유와 고를 것
|
||||
|
|
@ -637,7 +705,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
host: _host,
|
||||
initialUrl: _url,
|
||||
dataDir: widget.c.settings.webViewDataDir,
|
||||
javaScript: widget.c.settings.webJavaScript)),
|
||||
javaScript: widget.c.settings.webJavaScript))),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/bookmarks_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/download_manager.dart';
|
||||
import 'package:jj_mkvmaker/app/net_guard.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/core/bookmarks.dart';
|
||||
import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
||||
import 'package:jj_mkvmaker/core/download_detect.dart';
|
||||
import 'package:jj_mkvmaker/core/web_address.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
|
|
@ -56,8 +58,9 @@ class _Nav implements WebNav {
|
|||
Future<void> back() async => backs++;
|
||||
@override
|
||||
Future<void> forward() async {}
|
||||
int reloads = 0;
|
||||
@override
|
||||
Future<void> reload() async {}
|
||||
Future<void> reload() async => reloads++;
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
var exported = 0;
|
||||
|
|
@ -302,6 +305,69 @@ void main() {
|
|||
d.dispose();
|
||||
});
|
||||
|
||||
testWidgets('250: DNS 서버에 닿지 않아 못 연 페이지는 엔진 오류 화면 대신 이유 · [시스템 DNS 로 바꾸기] [다시 시도]', (tester) async {
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_browser_dns_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final g = NetGuard.instance;
|
||||
addTearDown(() => g.failure.value = null);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
late BrowserHost host;
|
||||
final nav = _Nav();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://blocked.example/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
const msg = 'DNS 서버 (192.0.2.1) 에 닿지 않아 blocked.example 을(를) 열 수 없습니다.';
|
||||
// 읽기 시작 → 프록시가 DNS 서버에 닿지 못함 → 끝 (엔진은 ERR_TUNNEL_CONNECTION_FAILED 화면)
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsOneWidget);
|
||||
// [다시 시도]: 덮개를 걷고 다시 읽기
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 1);
|
||||
// 다른 이름은 막지 않는다 · 서버가 다시 답한 뒤에는 지난 실패로 막지 않는다
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://ok.example/');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('에 닿지 않아'), findsNothing);
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
g.resolver.onAnswer?.call('blocked.example', '192.0.2.1');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
// [시스템 DNS 로 바꾸기]
|
||||
host.onProgress(0.1);
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('시스템 DNS 로 바꾸기'));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(c.settings.dnsMode, 'system');
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 2);
|
||||
});
|
||||
|
||||
testWidgets('Android 뒤로 키: 앞 웹 페이지가 있으면 웹 뒤로, 없으면 화면 닫기 · 위쪽 ← 는 늘 닫기', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue