WIP 관리자 결정: 거절 기록은 방식 주소:포트만 · DoH 주소는 호스트만 · 시험 임시 폴더 흔적 0 + 검사 (검토 대기)
관리자 결정 (10/17, 개인정보):
- DNS 프록시 거절 기록: 요청 줄 대신 "방식 주소:포트" 만 (경로 · 질의 (? 뒤) 는 남기지 않음, requestLineForLog).
- DoH 주소: 작업 기록 · app.log · 안내 · 다운로드 실패 이유에는 호스트만 ("https://dns.nextdns.io/…", DnsConfig.label · hideUrlPaths).
DoH 오류 원문 안의 주소도 가림. 설정 화면은 그대로.
시험 임시 폴더 (%TEMP%\jj_*) - 전체 시험마다 8개가 남았다:
- 앱 버그 (버그 수정): AI 해상도 올리기 upscale4x 가 실패 · 취소하면 jj_up_ 임시 폴더를 지우지 않음 → 지움.
품질 올리기 (묶음6 내 코드) 는 4배 결과 폴더를 지우지 않음 → 저장했든 못 했든 지움. 사용자 기기의 임시 폴더에도 남던 것.
- 시험: 책갈피 저장이 뒤에서 늦게 끝나 폴더를 다시 만들던 3곳 (jj_bmf_ · jj_bm_ · jj_nav_) 은 파일 끝에 기다렸다 지움
(test/support/temp_dirs.dart). ai_upscale_test 는 받은 결과 폴더를 지움.
- 검사: tool/test_temp_check.ps1 - 전체 시험 앞뒤로 %TEMP% 를 비교해 시험 소스의 jj… 이름으로 새로 남은 것이 있으면 실패.
(시험 파일들이 동시에 돌아 파일마다 스스로 확인할 수 없어 전체 실행 앞뒤로 본다.) 이미 있던 폴더는 건드리지 않음.
전체 822 통과 · 17 건너뜀, 새로 남은 임시 폴더 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
parent
a223a10378
commit
ab4d935fca
|
|
@ -191,6 +191,22 @@ class AiUpscaler {
|
|||
_cancelled = false;
|
||||
final tmp = await Directory.systemTemp.createTemp('jj_up_');
|
||||
final out = p.join(tmp.path, 'up.png');
|
||||
// 결과를 돌려주지 못하면 (실패 · 취소) 임시 폴더를 지운다 - 돌려준 결과는 부른 쪽이 다 쓴 뒤 지운다
|
||||
var done = false;
|
||||
try {
|
||||
final r = await _upscaleInto(input, out, onProgress: onProgress, onEta: onEta);
|
||||
done = true;
|
||||
return r;
|
||||
} finally {
|
||||
if (!done) {
|
||||
try {
|
||||
await tmp.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _upscaleInto(String input, String out, {void Function(double? p)? onProgress, void Function(Duration? left)? onEta}) async {
|
||||
try {
|
||||
// 176: 큰 그림 · 메모리가 적은 기기는 작은 타일 (메모리를 덜 쓴다)
|
||||
int? longSide;
|
||||
|
|
|
|||
|
|
@ -34,10 +34,16 @@ class DnsConfig {
|
|||
/// 캐시 열쇠 (설정이 바뀌면 예전 답을 쓰지 않게)
|
||||
String get key => '$mode|$primary|$secondary|${mode == 'doh' ? dohUrl : ''}';
|
||||
|
||||
/// 화면에 보일 이름: "8.8.8.8, 8.8.4.4" · DoH 주소
|
||||
String get label => mode == 'doh' ? dohUrl : [primary, secondary].where((s) => s.trim().isNotEmpty).join(', ');
|
||||
/// 화면 · 작업 기록에 보일 이름: "8.8.8.8, 8.8.4.4" · DoH 주소는 호스트만 ("https://dns.nextdns.io/…", [hideUrlPaths]).
|
||||
/// 설정 화면은 [dohUrl] 을 그대로 보인다
|
||||
String get label => mode == 'doh' ? hideUrlPaths(dohUrl) : [primary, secondary].where((s) => s.trim().isNotEmpty).join(', ');
|
||||
}
|
||||
|
||||
/// 글 안의 http(s) 주소에서 경로 · 질의를 가린다: "https://dns.nextdns.io/abc123" → "https://dns.nextdns.io/…".
|
||||
/// NextDNS 처럼 DoH 주소의 경로에 개인 ID 가 있어 작업 기록 · app.log · 안내에 남기지 않는다 (관리자 결정 10/17)
|
||||
String hideUrlPaths(String s) =>
|
||||
s.replaceAllMapped(RegExp(r'(https?://[^\s/?#)\]]+)[/?#][^\s)\]]*', caseSensitive: false), (m) => m[0]!.length == m[1]!.length + 1 ? m[0]! : '${m[1]}/…');
|
||||
|
||||
/// DNS 서버 주소 (IPv4 · IPv6) 를 읽는다. 틀리면 null
|
||||
InternetAddress? parseDnsServer(String s) {
|
||||
var t = s.trim();
|
||||
|
|
@ -194,7 +200,7 @@ class DnsResolver {
|
|||
|
||||
/// 서버들에 차례로 묻는다 (첫 서버가 답하지 않으면 다음 서버)
|
||||
Future<(DnsAnswer, String)> _ask(DnsConfig c, String name, int type) async {
|
||||
if (c.mode == 'doh') return (await _doh(c, name, type), c.dohUrl);
|
||||
if (c.mode == 'doh') return (await _doh(c, name, type), c.label);
|
||||
final servers = c.servers;
|
||||
if (servers.isEmpty) {
|
||||
throw DnsException(tr('DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)'), unreachable: true);
|
||||
|
|
@ -301,14 +307,14 @@ class DnsResolver {
|
|||
body.add(chunk);
|
||||
}
|
||||
if (res.statusCode != 200) {
|
||||
throw DnsException(trf('DoH 서버 ({0}) 가 HTTP {1} 로 답했습니다', [c.dohUrl, res.statusCode]),
|
||||
unreachable: true, server: c.dohUrl);
|
||||
throw DnsException(trf('DoH 서버 ({0}) 가 HTTP {1} 로 답했습니다', [c.label, res.statusCode]),
|
||||
unreachable: true, server: c.label);
|
||||
}
|
||||
return parseDnsResponse(body.toBytes());
|
||||
} on DnsException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw DnsException(trf('DoH 서버 ({0}) 에 닿지 않습니다: {1}', [c.dohUrl, e]), unreachable: true, server: c.dohUrl);
|
||||
throw DnsException(hideUrlPaths(trf('DoH 서버 ({0}) 에 닿지 않습니다: {1}', [c.label, e])), unreachable: true, server: c.label);
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,6 +235,14 @@ class ProxyAuth {
|
|||
}
|
||||
}
|
||||
|
||||
/// 거절 기록에 남길 요청: "방식 주소:포트" 만 - 경로 · 질의 (? 뒤) 는 남기지 않는다 (관리자 결정 10/17, 개인정보)
|
||||
String requestLineForLog(String method, String target) {
|
||||
if (method == 'CONNECT') return '$method ${target.split('/').first}';
|
||||
final u = Uri.tryParse(target);
|
||||
if (u == null || u.host.isEmpty) return '$method ?';
|
||||
return '$method ${u.host.contains(':') ? '[${u.host}]' : u.host}:${u.port}';
|
||||
}
|
||||
|
||||
/// 앱 안 HTTP 프록시 (127.0.0.1 의 빈 포트). CONNECT (https) 와 http 요청을 받아 [connector] 로 잇는다.
|
||||
/// - DNS 강제: [DnsConnector] - 웹 브라우저 · yt-dlp · aria2 · 앱의 HTTP 가 이리로
|
||||
/// - Tor 화면: [TorConnector] - 웹 뷰가 이리로 (이름 풀기까지 Tor 안에서)
|
||||
|
|
@ -251,7 +259,7 @@ class LocalProxy {
|
|||
/// 요구하는 인증 (null 이면 묻지 않음 - 시험용). 맞지 않으면 407
|
||||
final ProxyAuth? auth;
|
||||
|
||||
/// 인증이 없거나 틀려 거절함 (요청 줄, 비밀값을 보냈는지) - 작업 기록 (비밀값 자체는 남기지 않는다)
|
||||
/// 인증이 없거나 틀려 거절함 ("방식 주소:포트" ([requestLineForLog]), 비밀값을 보냈는지) - 작업 기록 (비밀값 자체는 남기지 않는다)
|
||||
void Function(String requestLine, bool hadCredentials)? onRejected;
|
||||
|
||||
LocalProxy(this.connector, {this.onError, this.auth});
|
||||
|
|
@ -314,7 +322,7 @@ class LocalProxy {
|
|||
.map((l) => l.substring(l.indexOf(':') + 1))
|
||||
.firstOrNull;
|
||||
if (!a.accepts(given)) {
|
||||
onRejected?.call(lines.first, given != null);
|
||||
onRejected?.call(requestLineForLog(method, target), given != null);
|
||||
client.add(latin1.encode('HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="JJ_MKVMaker"\r\n'
|
||||
'Content-Length: 0\r\nConnection: close\r\n\r\n'));
|
||||
await client.flush();
|
||||
|
|
|
|||
|
|
@ -528,10 +528,17 @@ Future<String?> qualityUp(BuildContext context, AppController c, {required Strin
|
|||
progress.value = x;
|
||||
prog(x);
|
||||
});
|
||||
try {
|
||||
if (cancelled) return;
|
||||
// 크기는 원본 기준 (설정의 배율, 1배면 원본 크기 그대로) · 이름_q
|
||||
final o = upscaleOptionsOf(c, outDir: outDir);
|
||||
saved = await saveUpscaled(four, source: nameFrom, original: input, o: o, suffix: '_q');
|
||||
} finally {
|
||||
// 4배 결과의 임시 폴더 (jj_up_) - 저장했든 못 했든 지운다
|
||||
try {
|
||||
await Directory(p.dirname(four)).delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}, onCancel: () {
|
||||
cancelled = true;
|
||||
engine.cancel();
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ void main() {
|
|||
final up = fake((i, o) => File(o).writeAsBytesSync(img.encodePng(img.Image(width: 32, height: 24))));
|
||||
final out = await up.upscale4x(input);
|
||||
expect(File(out).existsSync(), true);
|
||||
// 돌려받은 결과의 임시 폴더는 부른 쪽이 지운다
|
||||
Directory(p.dirname(out)).deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('엔진 기록의 실패 줄을 알아챈다 (0 으로 끝나도)', () {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import 'package:jj_mkvmaker/ui/home_page.dart';
|
|||
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dirs.dart';
|
||||
|
||||
class _Recognizer implements SpeechRecognizer {
|
||||
@override
|
||||
void cancel() {}
|
||||
|
|
@ -115,6 +117,7 @@ AppController _plain() =>
|
|||
AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
|
||||
void main() {
|
||||
deleteTempDirsAtEnd();
|
||||
test('여러 개 선택: 체크 · 전체 선택 · 일괄 대상 · 선택 제거', () async {
|
||||
final dir = Directory.systemTemp.createTempSync('jj_batch_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
|
@ -668,7 +671,7 @@ void main() {
|
|||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_nav_');
|
||||
final dir = tempDirUntilEnd('jj_nav_');
|
||||
final c = _plain();
|
||||
final d = DownloadManager(backends: [_Backend()], settings: () => c.settings, readClipboard: () async => null);
|
||||
final bm = BookmarksController(p.join(dir.path, 'bm.json'));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart' show kSecondaryButton;
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -8,13 +7,16 @@ import 'package:jj_mkvmaker/core/bookmarks.dart';
|
|||
import 'package:jj_mkvmaker/ui/bookmark_ui.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dirs.dart';
|
||||
|
||||
BookmarksController _bm() {
|
||||
// 저장은 기다리지 않고 뒤에서 하므로, 시험이 끝나도 폴더를 바로 지우지 않는다 (임시 폴더)
|
||||
final dir = Directory.systemTemp.createTempSync('jj_bmf_');
|
||||
// 저장은 기다리지 않고 뒤에서 하므로, 시험이 끝나도 폴더를 바로 지우지 않는다 - 파일 끝에 지운다
|
||||
final dir = tempDirUntilEnd('jj_bmf_');
|
||||
return BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
}
|
||||
|
||||
void main() {
|
||||
deleteTempDirsAtEnd();
|
||||
test('폴더 안에 폴더 · 즐겨찾기, 경로 · 검색 · 개수, 폴더로 옮기기 규칙', () {
|
||||
final bm = _bm();
|
||||
final music = bm.addFolder('음악');
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import 'package:jj_mkvmaker/ui/browser_page.dart';
|
|||
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dirs.dart';
|
||||
|
||||
class _Backend implements DownloadBackend {
|
||||
@override
|
||||
final DownloadKind kind;
|
||||
|
|
@ -99,6 +101,7 @@ class _ProbeState extends State<_Probe> {
|
|||
}
|
||||
|
||||
void main() {
|
||||
deleteTempDirsAtEnd();
|
||||
group('즐겨찾기 트리', () {
|
||||
test('추가 · 찾기 · 이동 · 삭제', () {
|
||||
final t = BookmarkTree.defaults();
|
||||
|
|
@ -479,7 +482,7 @@ void main() {
|
|||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_bm_');
|
||||
final dir = tempDirUntilEnd('jj_bm_');
|
||||
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
|
|
|
|||
|
|
@ -105,6 +105,21 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('작업 기록: DoH 주소는 호스트만 · 거절 기록은 방식 주소:포트만 (관리자 결정 10/17, 개인정보)', () {
|
||||
const doh = DnsConfig(mode: 'doh', dohUrl: 'https://dns.nextdns.io/abc123');
|
||||
expect(doh.label, 'https://dns.nextdns.io/…');
|
||||
expect(doh.dohUrl, 'https://dns.nextdns.io/abc123', reason: '설정 값은 그대로');
|
||||
expect(const DnsConfig(mode: 'doh', dohUrl: 'https://dns.google').label, 'https://dns.google');
|
||||
expect(const DnsConfig(mode: 'custom', primary: '8.8.8.8', secondary: '8.8.4.4').label, '8.8.8.8, 8.8.4.4');
|
||||
expect(hideUrlPaths('DoH 서버 (x) 에 닿지 않습니다: HttpException: closed, uri = https://dns.nextdns.io:443/abc123?dns=AAA'),
|
||||
'DoH 서버 (x) 에 닿지 않습니다: HttpException: closed, uri = https://dns.nextdns.io:443/…');
|
||||
expect(hideUrlPaths('(https://a.example/p/q) · http://b.example/'), '(https://a.example/…) · http://b.example/');
|
||||
expect(requestLineForLog('CONNECT', 'www.example.com:443'), 'CONNECT www.example.com:443');
|
||||
expect(requestLineForLog('GET', 'http://www.example.com/secret/path?token=abc'), 'GET www.example.com:80');
|
||||
expect(requestLineForLog('POST', 'https://api.example.com:8443/x?y=1'), 'POST api.example.com:8443');
|
||||
expect(requestLineForLog('GET', '/local?q=1'), 'GET ?');
|
||||
});
|
||||
|
||||
test('DNS 질문 · 답 읽기 (줄임 표시)', () {
|
||||
final q = buildDnsQuery('a.example.com', 1, id: 0x1234);
|
||||
expect(q.sublist(0, 2), [0x12, 0x34]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// 뒤에서 저장이 끝나는 것 (책갈피 저장 등) 을 기다렸다가 지울 임시 폴더 - 시험 파일이 끝날 때 지운다.
|
||||
/// 시험마다 바로 지우면 늦게 끝난 저장이 폴더를 다시 만들어 %TEMP% 에 jj_* 가 남았다 (관리자 지시 10/17, tool/test_temp_check.ps1).
|
||||
final _later = <Directory>[];
|
||||
|
||||
/// [prefix] 로 임시 폴더를 만들고, 파일 끝 ([deleteTempDirsAtEnd]) 에 지우도록 적어 둔다
|
||||
Directory tempDirUntilEnd(String prefix) {
|
||||
final d = Directory.systemTemp.createTempSync(prefix);
|
||||
_later.add(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
/// main() 에서 한 번 부른다: 파일의 시험이 다 끝나면 [tempDirUntilEnd] 로 만든 폴더를 지운다
|
||||
/// (늦게 끝난 저장이 다시 만들 수 있어 잠깐씩 기다리며 몇 번 본다)
|
||||
void deleteTempDirsAtEnd() {
|
||||
tearDownAll(() async {
|
||||
for (var round = 0; round < 5; round++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
for (final d in _later) {
|
||||
try {
|
||||
if (d.existsSync()) d.deleteSync(recursive: true);
|
||||
} on FileSystemException {
|
||||
// 잠깐 잡혀 있으면 다음 차례에
|
||||
}
|
||||
}
|
||||
}
|
||||
_later.removeWhere((d) => !d.existsSync());
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# 전체 시험 + 임시 폴더 흔적 검사 (관리자 지시 10/17): 시험이 %TEMP%\jj_* 폴더를 남기지 않는지.
|
||||
# - 시험 전 · 뒤의 %TEMP% 를 비교해, 새로 남은 폴더 중 이름 앞부분이 시험 소스 (test\*.dart) 의 createTemp('jj…') 와 같은 것만 본다
|
||||
# (앱 · 다른 프로그램이 만든 jj_mkvmaker 등은 세지 않음). 하나라도 남으면 이름을 보이고 실패 (exit 2).
|
||||
# - 시험 파일들은 동시에 돌아 파일마다 스스로 확인할 수 없어 (누가 만든 폴더인지 모름) 전체 실행 앞뒤로 본다.
|
||||
# - 다른 worktree · 세션이 같은 시험을 동시에 돌리면 그쪽 폴더가 섞일 수 있다 (이름으로 확인).
|
||||
# - 이미 있던 폴더는 건드리지 않는다 (지우지 않음).
|
||||
# 사용 (app 폴더에서): powershell -ExecutionPolicy Bypass -File tool\test_temp_check.ps1 [flutter test 인자…]
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$app = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
$temp = [System.IO.Path]::GetTempPath()
|
||||
|
||||
# 시험 소스가 쓰는 임시 폴더 이름 앞부분 (createTemp('jj_x_') · createTempSync('jj_x_'))
|
||||
$prefixes = @(Get-ChildItem (Join-Path $app 'test') -Recurse -Filter *.dart |
|
||||
ForEach-Object { [regex]::Matches((Get-Content $_.FullName -Raw -Encoding utf8), 'createTemp(?:Sync)?\(''(jj[^''$]*)''') } |
|
||||
ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
|
||||
if ($prefixes.Count -eq 0) { throw '시험 소스에서 임시 폴더 이름을 찾지 못했습니다' }
|
||||
|
||||
function Get-TestTemp {
|
||||
@(Get-ChildItem $temp -Directory -Filter 'jj*' -ErrorAction SilentlyContinue | ForEach-Object { $_.Name })
|
||||
}
|
||||
|
||||
$before = @{}
|
||||
foreach ($n in Get-TestTemp) { $before[$n] = $true }
|
||||
Push-Location $app
|
||||
try {
|
||||
flutter test @args
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
# 시험이 끝난 뒤 지우기가 늦게 끝나는 것 (백신 · 색인) 을 잠깐 기다린다
|
||||
Start-Sleep -Seconds 3
|
||||
$left = @(Get-TestTemp | Where-Object { -not $before.ContainsKey($_) } | Where-Object {
|
||||
$n = $_
|
||||
@($prefixes | Where-Object { $n.StartsWith($_) }).Count -gt 0
|
||||
})
|
||||
if ($left.Count -gt 0) {
|
||||
Write-Host ("시험이 남긴 임시 폴더 {0}개 (%TEMP%):" -f $left.Count)
|
||||
$left | Sort-Object | ForEach-Object { Write-Host " $_" }
|
||||
if ($code -eq 0) { exit 2 }
|
||||
} else {
|
||||
Write-Host '임시 폴더 흔적: 새로 남은 것 0'
|
||||
}
|
||||
exit $code
|
||||
Loading…
Reference in New Issue