M1-1: "Android 인 척" 스위치 · 실패 흉내 플래그를 주입으로 (PlatformInfo · WebViewProxyTarget)
- MemorySecretStore.failRead/failWrite 를 없앰 → 시험 쪽 test/support/flaky_secret_store.dart - WebProxy.debugAndroid/debugSetProxy 를 없앰 → WebViewProxyTarget 주입 (Android=ProxyController, 그 밖=없음) - ExitGuard.forceAndroid 를 없앰 → PlatformServices.platform (PlatformInfo) - JJ_NLLB_LOW_MEMORY 환경 변수를 없앰 → NllbTranslator(lowMemory:), 처음 값은 PlatformInfo.host - archive_ops · rsync_launch · file_error 의 기본값을 PlatformInfo.host 로 모음 - 이름 바꿈: WebProxy.shownForTest → shown, debugReset → reset - 보안: 남은 Tor 웹 뷰가 있는 동안에도 removeImplicitRules (localhost 까지 Tor 로) - 약속 시험 platform_info_contract_test 3개 확인: analyze 0 · 단위 1470 ×2 · 통합 6/6 ×2 · base10 214장 0px Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
39d81f1a73
commit
2199d7c15f
|
|
@ -3,12 +3,12 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
import '../core/local_proxy.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'net_guard.dart';
|
||||
import 'tor_service.dart';
|
||||
import '../services/platform_info.dart';
|
||||
import 'web_view_proxy_target.dart';
|
||||
|
||||
/// Android 웹 뷰 프록시 (androidx.webkit ProxyController - 앱의 모든 웹 뷰에 한꺼번에 걸린다).
|
||||
/// - Tor 화면이 열려 있으면: 모든 웹 뷰가 Tor 로 (관리자 결정 10/10 - 일반 브라우저에는 안내 띠)
|
||||
|
|
@ -63,7 +63,7 @@ class WebProxy {
|
|||
/// 보이는 웹 뷰 화면 수 ([show] · [hide]) 와 앱이 앞에 있는지
|
||||
static int _shown = 0;
|
||||
static bool _foreground = true;
|
||||
static int get shownForTest => _shown;
|
||||
static int get shown => _shown;
|
||||
|
||||
/// 웹 뷰 문 (인증 없음): 정한 DNS 로 잇는 것 · Tor 로 잇는 것 (따로 - Tor 화면을 닫으면 Tor 문도 닫혀 다른 길로 새지 않게)
|
||||
static LocalProxy? _dnsGate;
|
||||
|
|
@ -155,12 +155,9 @@ class WebProxy {
|
|||
}
|
||||
}
|
||||
|
||||
/// 시험용: Android 처럼 돌리고 ([debugAndroid]), 웹 뷰에 프록시를 거는 대신 기록 ([debugSetProxy], '' = 없앰)
|
||||
@visibleForTesting
|
||||
static bool debugAndroid = false;
|
||||
@visibleForTesting
|
||||
static Future<void> Function(String url, bool removeImplicitRules)? debugSetProxy;
|
||||
static bool get _android => Platform.isAndroid || debugAndroid;
|
||||
/// 웹 뷰에 프록시를 거는 곳 (이 기기에 맞는 것 - Windows 는 쓰지 않음). 조립 · 시험이 바꿔 끼운다 (M1)
|
||||
static WebViewProxyTarget target = WebViewProxyTarget.forPlatform(PlatformInfo.host);
|
||||
static bool get _android => target.active;
|
||||
|
||||
/// 지금 상태에 맞게 다시 건다 (차례로 - 겹치지 않게). 끝나면 걸린 프록시
|
||||
static Future<String> apply() {
|
||||
|
|
@ -191,32 +188,16 @@ class WebProxy {
|
|||
await closeUnused();
|
||||
return _applied;
|
||||
}
|
||||
if (debugSetProxy case final f?) {
|
||||
await f(want, tor);
|
||||
_applied = want;
|
||||
await closeUnused();
|
||||
return want;
|
||||
}
|
||||
final pc = ProxyController.instance();
|
||||
if (want.isEmpty) {
|
||||
await pc.clearProxyOverride();
|
||||
} else {
|
||||
await pc.setProxyOverride(
|
||||
settings: ProxySettings(
|
||||
proxyRules: [ProxyRule(url: want)],
|
||||
// Tor 화면: localhost 까지 Tor 로 (이 기기 · 집 안을 들여다보지 못하게). DNS 강제: 기본 (이 기기 주소는 직접)
|
||||
removeImplicitRules: torOpen.value,
|
||||
));
|
||||
}
|
||||
// Tor 화면: localhost 까지 Tor 로 (이 기기 · 집 안을 들여다보지 못하게). DNS 강제: 기본 (이 기기 주소는 직접)
|
||||
await target.set(want, removeImplicitRules: tor);
|
||||
_applied = want;
|
||||
debugPrint(trf('웹 뷰 프록시: {0}', [want.isEmpty ? tr('없음') : want]));
|
||||
await closeUnused();
|
||||
return want;
|
||||
}
|
||||
|
||||
/// 시험용: 처음 상태로
|
||||
@visibleForTesting
|
||||
static Future<void> debugReset() async {
|
||||
/// 처음 상태로 (문을 모두 닫고 수를 0 으로)
|
||||
static Future<void> reset() async {
|
||||
_shown = 0;
|
||||
_foreground = true;
|
||||
_applied = '';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/// 약속: 앱의 모든 웹 뷰에 프록시를 거는 곳 ([WebProxy] 가 부른다).
|
||||
/// 입력 = 프록시 주소 ('' = 없앰) · Tor 처럼 localhost 까지 프록시로 보낼지. 출력 없음. 오류 = 기기가 거절하면 그 예외.
|
||||
/// [active] 가 false 인 기기 (Windows - 웹 뷰마다 따로 건다) 에서는 [WebProxy] 가 아무것도 하지 않는다
|
||||
library;
|
||||
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
import '../services/platform_info.dart';
|
||||
|
||||
abstract class WebViewProxyTarget {
|
||||
/// 이 기기에서 앱 전체 웹 뷰 프록시를 쓰는지
|
||||
bool get active;
|
||||
|
||||
Future<void> set(String url, {required bool removeImplicitRules});
|
||||
|
||||
/// 이 기기에 맞는 것 (Android: androidx.webkit ProxyController, 아니면 쓰지 않음)
|
||||
factory WebViewProxyTarget.forPlatform(PlatformInfo platform) =>
|
||||
platform.isAndroid ? const AndroidWebViewProxy() : const NoWebViewProxy();
|
||||
}
|
||||
|
||||
/// Android: androidx.webkit ProxyController - 앱의 모든 웹 뷰에 한꺼번에 걸린다
|
||||
class AndroidWebViewProxy implements WebViewProxyTarget {
|
||||
const AndroidWebViewProxy();
|
||||
@override
|
||||
bool get active => true;
|
||||
|
||||
@override
|
||||
Future<void> set(String url, {required bool removeImplicitRules}) async {
|
||||
final pc = ProxyController.instance();
|
||||
if (url.isEmpty) {
|
||||
await pc.clearProxyOverride();
|
||||
} else {
|
||||
await pc.setProxyOverride(
|
||||
settings: ProxySettings(proxyRules: [ProxyRule(url: url)], removeImplicitRules: removeImplicitRules));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 앱 전체 웹 뷰 프록시가 없는 기기 (Windows)
|
||||
class NoWebViewProxy implements WebViewProxyTarget {
|
||||
const NoWebViewProxy();
|
||||
@override
|
||||
bool get active => false;
|
||||
@override
|
||||
Future<void> set(String url, {required bool removeImplicitRules}) async {}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:path/path.dart' as p;
|
|||
|
||||
import '../l10n/tr.dart';
|
||||
import 'file_ops.dart' show NameConflict, nameProblem;
|
||||
import '../services/platform_info.dart';
|
||||
|
||||
/// 86: 압축 종류 (확장자로)
|
||||
enum ArchiveKind { zip, sevenZip, rar }
|
||||
|
|
@ -82,7 +83,7 @@ class ExtractPlan {
|
|||
/// 목록 [entries] 에서 풀 것을 고른다. [names] 가 있으면 그 이름 (폴더면 그 안 모두) 만
|
||||
/// 15 지적 · 관리자 판단: 리눅스 · 맥에서 만든 압축의 이름이 Windows 에서 쓸 수 없으면 (78 의 nameProblem) 미리 걸러 건너뛴다
|
||||
ExtractPlan planExtract(List<ArchiveEntry> entries, {List<String> names = const [], bool? windowsRules}) {
|
||||
final win = windowsRules ?? Platform.isWindows;
|
||||
final win = windowsRules ?? PlatformInfo.host.isWindows;
|
||||
bool wanted(String n) => names.isEmpty || names.any((x) => n == x || n.startsWith(x.endsWith('/') ? x : '$x/'));
|
||||
final files = <ArchiveEntry>[];
|
||||
final unsafe = <(String, String)>[];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import '../services/platform_info.dart';
|
||||
|
||||
/// 관리자 지시 (10/20, 28 재현 - P0 데이터): Windows 의 rsync (MSYS2 · Cygwin 런타임) 는 Windows 프로그램이 넘긴 명령 줄을
|
||||
/// 스스로 다시 읽는다 - ' 를 따옴표로 짝지어 (--exclude=it's.txt --exclude=Bob's.txt 가 한 인수로 합쳐져 거르기가 사라지고
|
||||
|
|
@ -32,7 +33,7 @@ List<String> msysUnsafeArgs(List<String> args) => [for (final a in args) if (a.c
|
|||
|
||||
/// [args] 를 rsync 에 그대로 넘기도록 준비한다 (Windows 만 응답 파일 · MSYS=noglob, 그 밖은 그대로). 끝나면 [RsyncLaunch.dispose]
|
||||
Future<RsyncLaunch> prepareRsyncLaunch(List<String> args, {bool? windows}) async {
|
||||
if (!(windows ?? Platform.isWindows)) return RsyncLaunch._(args, null, null, null);
|
||||
if (!(windows ?? PlatformInfo.host.isWindows)) return RsyncLaunch._(args, null, null, null);
|
||||
final bad = msysUnsafeArgs(args);
|
||||
if (bad.isNotEmpty) {
|
||||
throw StateError(trf('큰따옴표 (") 가 든 인수는 Windows 의 rsync 에 그대로 넘길 수 없어 실행하지 않았습니다: {0}', [bad.join(' · ')]));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:path/path.dart' as p;
|
|||
|
||||
import '../../core/nllb_tokenizer.dart';
|
||||
import '../../services/ai_services.dart';
|
||||
import '../../services/platform_info.dart';
|
||||
import 'onnx_external.dart';
|
||||
import 'ort_tensors.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
|
|
@ -29,7 +30,10 @@ class NllbTranslator implements Translator {
|
|||
/// 한 번에 번역할 줄 수
|
||||
final int batchSize;
|
||||
|
||||
NllbTranslator({this.batchSize = 8});
|
||||
/// 메모리를 아끼는 방식 (모델을 하나씩 · 외부 데이터로 연다). 처음 값 = 이 기기 ([PlatformInfo.lowMemory])
|
||||
final bool lowMemory;
|
||||
|
||||
NllbTranslator({this.batchSize = 8, bool? lowMemory}) : lowMemory = lowMemory ?? PlatformInfo.host.lowMemory;
|
||||
|
||||
Isolate? _isolate;
|
||||
SendPort? _toWorker;
|
||||
|
|
@ -76,7 +80,7 @@ class NllbTranslator implements Translator {
|
|||
final threads = math.max(1, Platform.numberOfProcessors ~/ 2);
|
||||
_isolate = await Isolate.spawn(
|
||||
_workerMain,
|
||||
[rp.sendPort, modelDir, threads, _cancelFlag.address, batchSize],
|
||||
[rp.sendPort, modelDir, threads, _cancelFlag.address, batchSize, lowMemory],
|
||||
debugName: 'nllb',
|
||||
);
|
||||
try {
|
||||
|
|
@ -143,13 +147,14 @@ void _workerMain(List<Object?> args) {
|
|||
final threads = args[2] as int;
|
||||
final cancel = ffi.Pointer<ffi.Int32>.fromAddress(args[3] as int);
|
||||
final batch = args[4] as int;
|
||||
final low = args[5] as bool;
|
||||
|
||||
final inbox = ReceivePort();
|
||||
main.send({'type': 'port', 'port': inbox.sendPort});
|
||||
|
||||
_NllbEngine engine;
|
||||
try {
|
||||
engine = _NllbEngine.load(modelDir, threads);
|
||||
engine = _NllbEngine.load(modelDir, threads, low);
|
||||
} catch (e) {
|
||||
main.send({'type': 'fatal', 'error': '$e'});
|
||||
inbox.close();
|
||||
|
|
@ -204,12 +209,10 @@ class _NllbEngine {
|
|||
|
||||
_NllbEngine(this.tok, this.dir, this.threads, this.lowMemory);
|
||||
|
||||
factory _NllbEngine.load(String dir, int threads) {
|
||||
factory _NllbEngine.load(String dir, int threads, bool low) {
|
||||
OrtEnv.instance.init();
|
||||
final tok = NllbTokenizer.fromJson(
|
||||
File(p.join(dir, 'tokenizer.json')).readAsStringSync());
|
||||
// JJ_NLLB_LOW_MEMORY=1: PC 에서 Android 방식을 시험할 때
|
||||
final low = Platform.isAndroid || Platform.environment['JJ_NLLB_LOW_MEMORY'] == '1';
|
||||
final e = _NllbEngine(tok, dir, threads, low);
|
||||
// 넉넉한 기기는 미리 둘 다 연다 (모델 파일이 깨졌으면 여기서 알 수 있게)
|
||||
if (!e.lowMemory) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/// 약속: 앱이 도는 기기가 무엇인지 (Android · Windows) 와 메모리를 아껴야 하는지 - 한곳에서만 정한다.
|
||||
/// 입력 없음 ([PlatformInfo.host] 는 이 기기를 그대로 읽는다), 출력 = 세 값, 오류 없음.
|
||||
/// 기기에 따라 다르게 도는 곳은 이것을 받아 (생성자 · 조립) 쓴다 - "Android 인 척" 하는 스위치를 따로 두지 않는다 (M1)
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
class PlatformInfo {
|
||||
final bool isAndroid;
|
||||
final bool isWindows;
|
||||
|
||||
/// 메모리를 아끼는 방식 (모델을 나눠 열기 등). 처음 값은 Android
|
||||
final bool lowMemory;
|
||||
|
||||
const PlatformInfo({required this.isAndroid, required this.isWindows, bool? lowMemory}) : lowMemory = lowMemory ?? isAndroid;
|
||||
|
||||
/// 이 기기
|
||||
static final PlatformInfo host = PlatformInfo(isAndroid: Platform.isAndroid, isWindows: Platform.isWindows);
|
||||
|
||||
static const android = PlatformInfo(isAndroid: true, isWindows: false);
|
||||
static const windows = PlatformInfo(isAndroid: false, isWindows: true);
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import 'downloader.dart';
|
|||
import 'media_player.dart';
|
||||
import 'media_tool.dart';
|
||||
import 'model_store.dart';
|
||||
import 'platform_info.dart';
|
||||
import 'preview_player.dart';
|
||||
import 'storage_service.dart';
|
||||
import 'subtitle_provider.dart';
|
||||
|
|
@ -63,6 +64,9 @@ class PlatformServices {
|
|||
/// PC 의 CPU · 메모리 사용량 (없으면 표시 숨김)
|
||||
final SystemUsage? usage;
|
||||
|
||||
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
||||
final PlatformInfo platform;
|
||||
|
||||
/// 인터넷 자막 사이트 (설정값을 읽는 함수를 받아 만든다)
|
||||
final List<SubtitleProvider> Function(AppSettings Function() settings)? createSubtitleProviders;
|
||||
|
||||
|
|
@ -79,7 +83,9 @@ class PlatformServices {
|
|||
this.usage,
|
||||
ModelStore? models,
|
||||
AppShell? shell,
|
||||
PlatformInfo? platform,
|
||||
}) : models = models ?? ModelStore(),
|
||||
platform = platform ?? PlatformInfo.host,
|
||||
shell = shell ?? NoopShell();
|
||||
|
||||
factory PlatformServices.create() {
|
||||
|
|
|
|||
|
|
@ -165,24 +165,14 @@ Uint8List _dpapi(Uint8List data, {required bool protect}) {
|
|||
class MemorySecretStore implements SecretStore {
|
||||
final Map<String, String> values = {};
|
||||
|
||||
/// 시험: 읽기 · 쓰기 실패 흉내
|
||||
bool failRead = false;
|
||||
bool failWrite = false;
|
||||
|
||||
@override
|
||||
String? lostCopy;
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> readAll() async {
|
||||
if (failRead) throw StateError('secret store unavailable');
|
||||
return Map.of(values);
|
||||
}
|
||||
Future<Map<String, String>> readAll() async => Map.of(values);
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async {
|
||||
if (failWrite) throw StateError('secret store write failed');
|
||||
values[key] = value;
|
||||
}
|
||||
Future<void> write(String key, String value) async => values[key] = value;
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async => values.remove(key);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
|
@ -44,10 +42,6 @@ class ExitGuard extends StatelessWidget {
|
|||
|
||||
static const _ch = MethodChannel('jj_mkvmaker/android');
|
||||
|
||||
/// 시험용: Android 가 아니어도 Android 처럼
|
||||
@visibleForTesting
|
||||
static bool forceAndroid = false;
|
||||
|
||||
/// 돌고 있는 작업 (글로)
|
||||
List<String> _running() {
|
||||
final live = LiveSync.instance;
|
||||
|
|
@ -61,7 +55,7 @@ class ExitGuard extends StatelessWidget {
|
|||
];
|
||||
}
|
||||
|
||||
bool get _guard => (Platform.isAndroid || forceAndroid) && !c.settings.run.inBackground && _running().isNotEmpty;
|
||||
bool get _guard => c.services.platform.isAndroid && !c.settings.run.inBackground && _running().isNotEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
|
||||
import '../core/secret_gate.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import '../services/platform_info.dart';
|
||||
|
||||
/// 파일 · WebDAV 오류를 사람이 읽을 말로 (제목, 할 일). 원문 예외는 알아볼 수 없을 때만 그대로.
|
||||
/// 파일 탐색기 · 폴더 고르기 · 그 밖에 WebDAV 를 읽는 화면이 함께 쓴다 (12 · 134).
|
||||
|
|
@ -10,7 +11,7 @@ const androidRestrictedError = 'ANDROID_RESTRICTED_FOLDER';
|
|||
|
||||
/// 50: Android 11 부터 어떤 파일 앱도 다른 앱의 데이터 폴더 (Android/data · Android/obb) 를 볼 수 없다
|
||||
bool isAndroidRestricted(String path, {bool? android}) =>
|
||||
(android ?? Platform.isAndroid) && RegExp(r'/Android/(data|obb)(/|$)', caseSensitive: false).hasMatch(path);
|
||||
(android ?? PlatformInfo.host.isAndroid) && RegExp(r'/Android/(data|obb)(/|$)', caseSensitive: false).hasMatch(path);
|
||||
|
||||
(String, String) explainFileError(String e, {required bool dav, bool noPassword = false}) {
|
||||
if (e == androidRestrictedError) {
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_info.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/exit_guard.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() => ExitGuard.forceAndroid = true);
|
||||
tearDown(() => ExitGuard.forceAndroid = false);
|
||||
|
||||
Future<AppController> pump(WidgetTester tester, {required bool background, required bool busy}) async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final c = AppController(PlatformServices(
|
||||
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), platform: PlatformInfo.android));
|
||||
c.settings.run.inBackground = background;
|
||||
c.busy = busy;
|
||||
c.currentJob = busy ? 'MKV 만들기 3/10' : null;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,15 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/master_lock.dart';
|
||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
|
||||
/// 읽기가 될 때와 안 될 때를 바꿀 수 있는 안전 저장소 (기기 저장소가 잠깐 잠긴 때처럼)
|
||||
class _FlakyStore implements SecretStore {
|
||||
final Map<String, String> values = {};
|
||||
bool readable = true;
|
||||
@override
|
||||
String? lostCopy;
|
||||
@override
|
||||
Future<Map<String, String>> readAll() async {
|
||||
if (!readable) throw StateError('안전 저장소를 읽을 수 없음');
|
||||
return Map.of(values);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async => values[key] = value;
|
||||
@override
|
||||
Future<void> delete(String key) async => values.remove(key);
|
||||
}
|
||||
import 'support/flaky_secret_store.dart';
|
||||
|
||||
/// M0 약속: 안전 저장소를 읽지 못하면 마스터 잠금은 '알 수 없음' - 문을 열지 않고 (한 번 다시 읽어 봄), 마스터 · 최상 · 묻는 때를 바꾸지 않는다.
|
||||
/// 다시 읽히면 평소대로 (마스터가 있으면 묻는다)
|
||||
void main() {
|
||||
late _FlakyStore store;
|
||||
late FlakySecretStore store;
|
||||
late int asked;
|
||||
setUp(() async {
|
||||
store = _FlakyStore();
|
||||
store = FlakySecretStore();
|
||||
// 마스터가 정해진 기기
|
||||
final setup = MasterLock(store);
|
||||
await setup.load();
|
||||
|
|
@ -40,7 +23,7 @@ void main() {
|
|||
tearDown(() => MasterLock.prompt = null);
|
||||
|
||||
test('읽지 못하면 문을 열지 않는다 (예전: 마스터가 없는 것으로 보여 열렸다)', () async {
|
||||
store.readable = false;
|
||||
store.failRead = true;
|
||||
final lock = MasterLock(store);
|
||||
await lock.load(); // 예외를 내지 않는다 - 앱은 켜진다
|
||||
expect([lock.storeUnreadable, lock.hasMaster], [true, false]);
|
||||
|
|
@ -51,7 +34,7 @@ void main() {
|
|||
|
||||
test('알 수 없는 동안 마스터 · 최상 · 묻는 때를 바꾸지 않는다 (진짜 마스터를 덮어쓰지 않게)', () async {
|
||||
final before = Map.of(store.values);
|
||||
store.readable = false;
|
||||
store.failRead = true;
|
||||
final lock = MasterLock(store);
|
||||
await lock.load();
|
||||
await expectLater(lock.setMaster('new-pass'), throwsA(isA<MasterStoreUnreadable>()));
|
||||
|
|
@ -62,20 +45,20 @@ void main() {
|
|||
});
|
||||
|
||||
test('다시 읽히면 평소대로: 문을 열 때 한 번 다시 읽고, 마스터가 있으면 묻는다', () async {
|
||||
store.readable = false;
|
||||
store.failRead = true;
|
||||
final lock = MasterLock(store);
|
||||
await lock.load();
|
||||
store.readable = true;
|
||||
store.failRead = false;
|
||||
expect(await lock.ensure(), isTrue);
|
||||
expect([lock.storeUnreadable, lock.hasMaster, asked], [false, true, 1]);
|
||||
});
|
||||
|
||||
test('reload: 읽혔는지 돌려준다', () async {
|
||||
store.readable = false;
|
||||
store.failRead = true;
|
||||
final lock = MasterLock(store);
|
||||
await lock.load();
|
||||
expect(await lock.reload(), isFalse);
|
||||
store.readable = true;
|
||||
store.failRead = false;
|
||||
expect(await lock.reload(), isTrue);
|
||||
expect(lock.hasMaster, isTrue);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/web_view_proxy_target.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_info.dart';
|
||||
|
||||
/// M1 약속: 기기 판단은 PlatformInfo 한곳 - 이 기기는 그대로 읽고, 메모리 아끼기 처음 값은 Android,
|
||||
/// 앱 전체 웹 뷰 프록시는 Android 에서만 (Windows 는 웹 뷰마다 따로)
|
||||
void main() {
|
||||
test('host 는 이 기기 그대로', () {
|
||||
expect([PlatformInfo.host.isAndroid, PlatformInfo.host.isWindows], [Platform.isAndroid, Platform.isWindows]);
|
||||
});
|
||||
|
||||
test('lowMemory 처음 값 = Android, 준 값은 그대로', () {
|
||||
expect([PlatformInfo.android.lowMemory, PlatformInfo.windows.lowMemory], [true, false]);
|
||||
expect(const PlatformInfo(isAndroid: false, isWindows: true, lowMemory: true).lowMemory, isTrue);
|
||||
expect(const PlatformInfo(isAndroid: true, isWindows: false, lowMemory: false).lowMemory, isFalse);
|
||||
});
|
||||
|
||||
test('웹 뷰 프록시: Android 만 쓰고, 아닌 기기는 걸어도 아무 일 없음', () async {
|
||||
expect(WebViewProxyTarget.forPlatform(PlatformInfo.android), isA<AndroidWebViewProxy>());
|
||||
final none = WebViewProxyTarget.forPlatform(PlatformInfo.windows);
|
||||
expect(none.active, isFalse);
|
||||
await none.set('http://127.0.0.1:9', removeImplicitRules: true);
|
||||
});
|
||||
}
|
||||
|
|
@ -8,16 +8,17 @@ import 'package:jj_mkvmaker/core/webdav.dart';
|
|||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
import 'package:jj_mkvmaker/ui/secret_issue.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/flaky_secret_store.dart';
|
||||
|
||||
/// 40-1 · 40-3: 비밀번호를 저장하지 못했으면 화면 위에 분명히 알리고 [다시 시도] 로 저장한다
|
||||
void main() {
|
||||
testWidgets('저장 실패 알림 → [다시 시도] 로 저장되면 알림이 사라진다', (tester) async {
|
||||
final dir = Directory.systemTemp.createTempSync('jj_secret_ui_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final secrets = MemorySecretStore()..failWrite = true;
|
||||
final secrets = FlakySecretStore()..failWrite = true;
|
||||
// 저장 순서용 Future 가 시험의 가짜 시간 영역에 묶이지 않게 실제 영역에서 만든다
|
||||
late SettingsStore store;
|
||||
late AppController c;
|
||||
|
|
@ -51,7 +52,7 @@ void main() {
|
|||
testWidgets('90 · 91: 예전 비밀번호로 돌아간다고 알림 · [닫기] 하면 같은 상황에서는 다시 안 뜸 · [다시 시도] 실패도 알림', (tester) async {
|
||||
final dir = Directory.systemTemp.createTempSync('jj_secret_ui2_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final secrets = MemorySecretStore()..values['dav.pw.d1'] = 'old';
|
||||
final secrets = FlakySecretStore()..values['dav.pw.d1'] = 'old';
|
||||
late SettingsStore store;
|
||||
late AppController c;
|
||||
await tester.runAsync(() async {
|
||||
|
|
|
|||
|
|
@ -5,18 +5,19 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/app/version_snapshot.dart';
|
||||
import 'package:jj_mkvmaker/core/webdav.dart';
|
||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/flaky_secret_store.dart';
|
||||
|
||||
/// 40 · 54: 비밀번호는 설정 파일에 평문으로 쓰지 않고 안전 저장소에. 한 번 넣으면 업데이트 · 예전 판 · 되살리기 뒤에도 남는다
|
||||
void main() {
|
||||
late Directory dir;
|
||||
late String file;
|
||||
late MemorySecretStore secrets;
|
||||
late FlakySecretStore secrets;
|
||||
setUp(() {
|
||||
dir = Directory.systemTemp.createTempSync('jj_secret_');
|
||||
file = p.join(dir.path, 'settings.json');
|
||||
secrets = MemorySecretStore();
|
||||
secrets = FlakySecretStore();
|
||||
});
|
||||
tearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
|
||||
/// 읽기 · 쓰기가 될 때와 안 될 때를 바꿀 수 있는 안전 저장소 (기기 저장소가 잠깐 잠긴 때처럼).
|
||||
/// 시험에만 있다 - lib 의 저장소에는 실패 흉내 플래그를 두지 않는다 (M1)
|
||||
class FlakySecretStore implements SecretStore {
|
||||
final Map<String, String> values = {};
|
||||
bool failRead = false;
|
||||
bool failWrite = false;
|
||||
|
||||
@override
|
||||
String? lostCopy;
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> readAll() async {
|
||||
if (failRead) throw StateError('secret store unavailable');
|
||||
return Map.of(values);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async {
|
||||
if (failWrite) throw StateError('secret store write failed');
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async => values.remove(key);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/net_guard.dart';
|
||||
import 'package:jj_mkvmaker/app/web_proxy.dart';
|
||||
import 'package:jj_mkvmaker/app/web_view_proxy_target.dart';
|
||||
import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
||||
|
||||
/// Android 웹 뷰 프록시 문 (관리자 결정 10/11 A+B): 인증 없이, 실행마다 고른 루프백 주소에,
|
||||
|
|
@ -12,18 +13,16 @@ import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
|||
void main() {
|
||||
final calls = <(String, bool)>[];
|
||||
setUp(() async {
|
||||
await WebProxy.debugReset();
|
||||
WebProxy.debugAndroid = true;
|
||||
WebProxy.debugSetProxy = (url, tor) async => calls.add((url, tor));
|
||||
await WebProxy.reset();
|
||||
WebProxy.target = _RecordingProxy(calls);
|
||||
WebProxy.torOpen.value = false;
|
||||
NetGuard.instance.config = () => const DnsConfig(mode: 'custom', primary: '8.8.8.8', secondary: '8.8.4.4', dohUrl: '');
|
||||
calls.clear();
|
||||
});
|
||||
tearDown(() async {
|
||||
WebProxy.torOpen.value = false;
|
||||
await WebProxy.debugReset();
|
||||
WebProxy.debugAndroid = false;
|
||||
WebProxy.debugSetProxy = null;
|
||||
await WebProxy.reset();
|
||||
WebProxy.target = const NoWebViewProxy();
|
||||
NetGuard.instance.config = () => const DnsConfig.system();
|
||||
});
|
||||
|
||||
|
|
@ -126,15 +125,15 @@ void main() {
|
|||
final nav = GlobalKey<NavigatorState>();
|
||||
await t.pumpWidget(MaterialApp(navigatorKey: nav, home: const _WebScreen()));
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 200)));
|
||||
expect(WebProxy.shownForTest, 1);
|
||||
expect(WebProxy.shown, 1);
|
||||
nav.currentState!.push(MaterialPageRoute<void>(builder: (_) => const Text('other')));
|
||||
await t.pumpAndSettle();
|
||||
expect(WebProxy.shownForTest, 0);
|
||||
expect(WebProxy.shown, 0);
|
||||
nav.currentState!.pop();
|
||||
await t.pumpAndSettle();
|
||||
expect(WebProxy.shownForTest, 1);
|
||||
expect(WebProxy.shown, 1);
|
||||
await t.pumpWidget(const SizedBox());
|
||||
expect(WebProxy.shownForTest, 0);
|
||||
expect(WebProxy.shown, 0);
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 200)));
|
||||
});
|
||||
}
|
||||
|
|
@ -149,3 +148,13 @@ class _WebScreenState extends State<_WebScreen> with WebProxyVisibility {
|
|||
@override
|
||||
Widget build(BuildContext context) => const Text('web');
|
||||
}
|
||||
|
||||
/// Android 처럼 앱 전체 웹 뷰 프록시를 쓰는 기기 - 거는 대신 기록 (시험 파일 안의 것, lib 에 흉내 장치 없음)
|
||||
class _RecordingProxy implements WebViewProxyTarget {
|
||||
final List<(String, bool)> calls;
|
||||
_RecordingProxy(this.calls);
|
||||
@override
|
||||
bool get active => true;
|
||||
@override
|
||||
Future<void> set(String url, {required bool removeImplicitRules}) async => calls.add((url, removeImplicitRules));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue