(가) 10곳: 시험이 앱 동작을 바꾸던 길을 주입 · 모듈로 (lib 의 @visibleForTesting 은 들여다보기 4곳만 남음)
- Tor: debugRunning · executable 없앰 → 떠난 뒤 잠시 켜 두기를 IdleStop 모듈로 (schedule · cancel · stopNow). 환경 설정 > Tor 는 IdleStop 을 받음 (처음 값 = 앱의 Tor), [지금 끄기] = stopNow - Wi-Fi 공유: debugLoopbackOnly · debugAddresses · loopbackOnly 인자 없앰 → WifiShareNet 주입 (열 주소 · 설정 열기). WifiSharePage(net:) 가 debugLaunch 를 대신함. debugFail 없앰 - 시험이 실제 다른 루프백 주소 (127.0.0.x) 에서 틀린 코드를 보냄 - 관리자 권한: Elevation.debugElevated 없앰 → PlatformInfo.elevated (조립 주입), CefView(elevated:) - CEF 시작 시간: 바꿀 수 없게 (const) - 시험은 가짜 시간을 넘김 - 단일 실행: debugStopListening 없앰 - 시험이 instance.json 을 아무도 듣지 않는 포트로 (받는 곳이 죽은 첫 창) - SMB 찾기: 전역 debugFindSmbServers 없앰 → PlatformServices.findLanSmb 주입 - 남은 @visibleForTesting (들여다보기만): browser_page · tor_page debugNav, secret_store dpapiAt, reader_page dropMissingFiles 확인: analyze 0 · 단위 1473 ×2 · 통합 6/6 ×2 · base10 214장 0px · windows_wifi_shot ×2 · windows_smbfind_shot ×2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
512c9d8b14
commit
eaaa096ea1
|
|
@ -28,17 +28,16 @@ void main() {
|
|||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
addTearDown(() => debugFindSmbServers = null);
|
||||
var hold = true;
|
||||
var result = const [LanHost('192.168.0.15', 'ds1815.local'), LanHost('192.168.0.20', null)];
|
||||
debugFindSmbServers = ({cancelled, onProgress}) async {
|
||||
Future<List<LanHost>> findLan({cancelled, onProgress}) async {
|
||||
onProgress?.call(97, 254);
|
||||
while (hold) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), findLanSmb: findLan));
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(RepaintBoundary(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import 'package:jj_mkvmaker/ui/wifi_share_page.dart';
|
|||
import 'support/temp_dir.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../test/support/loopback_net.dart';
|
||||
|
||||
/// 관리자 지시 (10/18): 89 Wi-Fi 공유 화면을 PNG 로 (JJ_SHOT_DIR) - 켜기 전 · 켠 뒤 (코드 · 주소 · QR · 남은 시간 · 접속 기록).
|
||||
/// 서버는 127.0.0.1 에만 연다 (시험 PC 의 방화벽 창 · 바깥 노출 없이), 주소 줄은 시험용 사설 주소로 바꿔 보인다.
|
||||
/// 크기 412 · 960, 언어 ko · en · ja. 실행: flutter test integration_test\windows_wifi_shot_test.dart -d windows
|
||||
|
|
@ -32,13 +34,8 @@ void main() {
|
|||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
WifiShare.debugLoopbackOnly = true;
|
||||
// 화면에는 이 PC 의 실제 주소 대신 시험용 사설 주소를 (캡처에 사무실 망 주소가 남지 않게)
|
||||
WifiShare.debugAddresses = const ['192.168.0.23'];
|
||||
addTearDown(() async {
|
||||
await WifiShare.instance?.stop();
|
||||
WifiShare.debugLoopbackOnly = false;
|
||||
WifiShare.debugAddresses = null;
|
||||
});
|
||||
final tmp = testTempDir('jj_wifishot_');
|
||||
final dir = Directory(p.join(tmp.path, '여행 사진'))..createSync();
|
||||
|
|
@ -46,7 +43,7 @@ void main() {
|
|||
final shotKey = GlobalKey();
|
||||
await t.pumpWidget(RepaintBoundary(
|
||||
key: shotKey,
|
||||
child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: WifiSharePage(c: c, folder: dir.path)),
|
||||
child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: WifiSharePage(c: c, folder: dir.path, net: LoopbackNet())),
|
||||
));
|
||||
Future<void> settle([int n = 8]) async {
|
||||
for (var i = 0; i < n; i++) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../core/local_proxy.dart';
|
|||
import '../core/tor_control.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'tor_bundle.dart';
|
||||
import '../core/idle_stop.dart';
|
||||
|
||||
/// Tor 실행 (Tor 화면이 열려 있는 동안). tor 를 자식 프로세스로 띄우고 (SOCKS · 제어 포트는 127.0.0.1 의 빈 포트),
|
||||
/// 웹 뷰가 쓸 HTTP 프록시 다리 ([LocalProxy] + [TorConnector]) 를 연다. 다리는 이름을 Tor 에 넘긴다 (이 기기의 DNS 를 쓰지 않음).
|
||||
|
|
@ -23,37 +24,25 @@ class TorService {
|
|||
|
||||
/// Tor 화면을 떠난 뒤 tor 를 끌 때 (기다리는 중이면 그 시각, 아니면 null) - 관리자 결정 10/11:
|
||||
/// 화면을 떠나면 웹 뷰 · 다리 · 프록시는 바로 정리하고 tor 만 잠시 (환경 설정, 처음 2분) 켜 둔다 (다시 들어오면 바로 쓰게)
|
||||
final idleStopAt = ValueNotifier<DateTime?>(null);
|
||||
Timer? _idleTimer;
|
||||
ValueNotifier<DateTime?> get idleStopAt => idle.stopAt;
|
||||
|
||||
/// [after] 뒤에 끈다 (0 이면 바로). 그 사이 [cancelIdleStop] 하면 그대로 켜 둔다
|
||||
/// 떠난 뒤 잠시 켜 두기 (시간 재기 · 지금 끄기) - 환경 설정 > Tor 가 이것을 받는다
|
||||
late final idle = IdleStop(stop);
|
||||
|
||||
/// [after] 뒤에 끈다 (0 이면 바로). 그 사이 [cancelIdleStop] 하면 그대로 켜 둔다 (시간 재기는 [IdleStop])
|
||||
Future<void> stopAfter(Duration after) async {
|
||||
cancelIdleStop();
|
||||
if (_proc == null && !debugRunning) return;
|
||||
if (after <= Duration.zero) return stop();
|
||||
idleStopAt.value = DateTime.now().add(after);
|
||||
log?.call(trf('Tor 대기 ({0}분 뒤 끔)', [after.inMinutes]));
|
||||
_idleTimer = Timer(after, () {
|
||||
_idleTimer = null;
|
||||
idleStopAt.value = null;
|
||||
unawaited(stop());
|
||||
});
|
||||
if (_proc == null) return;
|
||||
if (after > Duration.zero) log?.call(trf('Tor 대기 ({0}분 뒤 끔)', [after.inMinutes]));
|
||||
await idle.schedule(after);
|
||||
}
|
||||
|
||||
/// 기다렸다 끄기를 그만둔다 (Tor 화면으로 돌아옴)
|
||||
void cancelIdleStop() {
|
||||
_idleTimer?.cancel();
|
||||
_idleTimer = null;
|
||||
idleStopAt.value = null;
|
||||
}
|
||||
void cancelIdleStop() => idle.cancel();
|
||||
|
||||
/// 작업 기록
|
||||
void Function(String)? log;
|
||||
|
||||
/// 시험용: 실행 파일 · 인수를 바꾼다
|
||||
@visibleForTesting
|
||||
Future<String?> Function() executable = TorBundle.executable;
|
||||
|
||||
Process? _proc;
|
||||
int? _socksPort;
|
||||
int? _controlPort;
|
||||
|
|
@ -85,11 +74,7 @@ class TorService {
|
|||
await b?.close();
|
||||
}
|
||||
|
||||
bool get running => _proc != null || debugRunning;
|
||||
|
||||
/// 시험용: tor 가 떠 있는 것처럼 ([stopAfter] 시험)
|
||||
@visibleForTesting
|
||||
bool debugRunning = false;
|
||||
bool get running => _proc != null;
|
||||
|
||||
/// 켜기 (이미 켜져 있거나 켜는 중이면 그것을 기다린다)
|
||||
Future<void> start() {
|
||||
|
|
@ -98,7 +83,7 @@ class TorService {
|
|||
}
|
||||
|
||||
Future<void> _start() async {
|
||||
final exe = await executable();
|
||||
final exe = await TorBundle.executable();
|
||||
if (exe == null) {
|
||||
_set(TorStatus(TorPhase.failed, message: tr('Tor 가 설치되어 있지 않습니다 (환경 설정 > 컴포넌트)')));
|
||||
return;
|
||||
|
|
@ -224,7 +209,6 @@ class TorService {
|
|||
/// 끄기 (Tor 화면을 닫을 때 · 앱이 끝날 때 · 제거할 때)
|
||||
Future<void> stop({bool keepStatus = false}) async {
|
||||
cancelIdleStop();
|
||||
debugRunning = false;
|
||||
_watchdog?.cancel();
|
||||
final proc = _proc;
|
||||
_proc = null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/// 약속: 정한 시간 뒤에 한 번 끈다 ([schedule]). 입력 = 기다릴 시간 (0 이하면 바로 끔), 출력 = 끌 시각 [stopAt] (기다리지 않으면 null).
|
||||
/// [cancel] 하면 끄지 않는다. [stopNow] 는 기다리지 않고 바로 끈다. 다시 [schedule] 하면 앞의 것은 잊는다. 오류 = 끄는 함수의 오류 (바로 끌 때만 돌려줌).
|
||||
/// 쓰는 곳: Tor (화면을 떠난 뒤 잠시 켜 둠 - 관리자 결정 10/11)
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class IdleStop {
|
||||
IdleStop(this.onStop);
|
||||
|
||||
final Future<void> Function() onStop;
|
||||
final stopAt = ValueNotifier<DateTime?>(null);
|
||||
Timer? _timer;
|
||||
|
||||
Future<void> schedule(Duration after) async {
|
||||
cancel();
|
||||
if (after <= Duration.zero) return onStop();
|
||||
stopAt.value = DateTime.now().add(after);
|
||||
_timer = Timer(after, () {
|
||||
_timer = null;
|
||||
stopAt.value = null;
|
||||
unawaited(onStop());
|
||||
});
|
||||
}
|
||||
|
||||
/// 기다리지 않고 지금 끈다 ([cancel] 뒤 끄는 함수)
|
||||
Future<void> stopNow() {
|
||||
cancel();
|
||||
return onStop();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
stopAt.value = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;
|
|||
import '../l10n/tr.dart';
|
||||
import 'file_ops.dart' show formatSize, isAppTempName, isPartialFile;
|
||||
import 'lan_scan.dart' show isLanAddress, isMobileInterfaceName, netKindsOf;
|
||||
import 'wifi_share_net.dart';
|
||||
|
||||
/// 89: Wi-Fi 공유 - 앱 안 작은 HTTP 서버로 고른 폴더 하나를 같은 Wi-Fi 의 다른 기기 브라우저에서 보기 · 받기 (읽기 전용).
|
||||
/// 사용자 결정 (10/9): 기본 꺼짐 · 읽기 전용 · 접속 비밀번호. 관리자 지시 (10/17):
|
||||
|
|
@ -37,14 +38,6 @@ class WifiShare extends ChangeNotifier {
|
|||
static const lockout = Duration(minutes: 1);
|
||||
static const totalFails = 30;
|
||||
|
||||
/// 시험: 화면 시험에서 이 기기 (127.0.0.1) 에만 연다 (시험 PC 의 방화벽 창 · 바깥 노출 없이)
|
||||
@visibleForTesting
|
||||
static bool debugLoopbackOnly = false;
|
||||
|
||||
/// 시험: 이 기기의 주소 대신 쓸 것 (화면 캡처에 시험 PC 의 실제 망 주소가 남지 않게)
|
||||
@visibleForTesting
|
||||
static List<String>? debugAddresses;
|
||||
|
||||
/// 화면에 보이는 접속 기록 개수
|
||||
static const maxLog = 200;
|
||||
|
||||
|
|
@ -79,20 +72,19 @@ class WifiShare extends ChangeNotifier {
|
|||
/// 접속 주소 (코드를 넣은 것 - QR 용)
|
||||
String urlWithCode(String host) => 'http://$host:$port/?code=$code';
|
||||
|
||||
/// 공유를 켠다. 이미 켜진 것이 있으면 먼저 끈다. [autoOff] 가 null 이면 저절로 끄지 않는다
|
||||
/// 공유를 켠다. 이미 켜진 것이 있으면 먼저 끈다. [autoOff] 가 null 이면 저절로 끄지 않는다. 열 주소는 [net] (처음 값 = 이 기기)
|
||||
static Future<WifiShare> start(String root,
|
||||
{Duration? autoOff, int port = defaultPort, Random? random, bool loopbackOnly = false, bool tailscale = true}) async {
|
||||
{Duration? autoOff, int port = defaultPort, Random? random, bool tailscale = true, WifiShareNet net = const RealWifiShareNet()}) async {
|
||||
await instance?.stop();
|
||||
final dir = Directory(root);
|
||||
if (!await dir.exists()) throw FileSystemException(tr('폴더가 없습니다'), root);
|
||||
final real = await dir.resolveSymbolicLinks();
|
||||
final rng = random ?? Random.secure();
|
||||
final code = List.generate(6, (_) => rng.nextInt(10)).join();
|
||||
final loop = loopbackOnly || debugLoopbackOnly;
|
||||
final addrs = debugAddresses ?? await localAddresses(tailscale: tailscale);
|
||||
final addrs = await net.addresses(tailscale: tailscale);
|
||||
// 보안 검토 3: 화면에 보이는 주소 (Wi-Fi · 유선 · Tailscale) 에만 연다 - 모바일 데이터 · 가상 어댑터 · 다른 VPN 에서는 열리지 않게
|
||||
if (!loop && addrs.isEmpty) throw SocketException(tr('Wi-Fi 나 유선 망에 연결되어 있지 않아 켤 수 없습니다'));
|
||||
final binds = loop ? ['127.0.0.1'] : addrs;
|
||||
if (addrs.isEmpty) throw SocketException(tr('Wi-Fi 나 유선 망에 연결되어 있지 않아 켤 수 없습니다'));
|
||||
final binds = addrs;
|
||||
final servers = <HttpServer>[];
|
||||
final shown = <String>[];
|
||||
try {
|
||||
|
|
@ -118,7 +110,7 @@ class WifiShare extends ChangeNotifier {
|
|||
server.defaultResponseHeaders.clear();
|
||||
server.serverHeader = null;
|
||||
}
|
||||
s.addresses = loop ? addrs : shown;
|
||||
s.addresses = shown;
|
||||
if (autoOff != null && autoOff > Duration.zero) {
|
||||
s.endsAt = s.started.add(autoOff);
|
||||
s._timer = Timer(autoOff, () => s.stop(reason: tr('정한 시간이 지나 저절로 껐습니다')));
|
||||
|
|
@ -345,10 +337,6 @@ class WifiShare extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
/// 시험: 틀린 코드 하나를 센다 (막힌 기기에서 온 것처럼 서버를 거치지 않고)
|
||||
@visibleForTesting
|
||||
void debugFail(String ip) => _fail(ip);
|
||||
|
||||
void _record(String ip, String path, int status) {
|
||||
if (_stopped) return;
|
||||
log.insert(0, WifiAccess(DateTime.now(), ip, path, status));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/// 약속: Wi-Fi 공유가 기기에서 받는 것 - 열 주소 ([addresses]: 사설망 먼저, 없으면 빈 목록) · Windows 설정 · 제어판 열기 ([launch]).
|
||||
/// 앱은 [RealWifiShareNet] (이 기기 그대로) 를 쓰고, 시험은 생성자로 다른 것을 넘긴다 (예: 127.0.0.1 에만 열기 - 시험 PC 의 방화벽 창 · 바깥 노출 없이)
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'wifi_share.dart' show WifiShare;
|
||||
|
||||
abstract interface class WifiShareNet {
|
||||
Future<List<String>> addresses({bool tailscale = true});
|
||||
Future<void> launch(String exe, List<String> args);
|
||||
}
|
||||
|
||||
/// 이 기기 그대로
|
||||
class RealWifiShareNet implements WifiShareNet {
|
||||
const RealWifiShareNet();
|
||||
@override
|
||||
Future<List<String>> addresses({bool tailscale = true}) => WifiShare.localAddresses(tailscale: tailscale);
|
||||
@override
|
||||
Future<void> launch(String exe, List<String> args) => Process.start(exe, args, mode: ProcessStartMode.detached);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef _IsAdminC = Int32 Function();
|
||||
typedef _IsAdminD = int Function();
|
||||
|
||||
|
|
@ -11,15 +9,10 @@ typedef _IsAdminD = int Function();
|
|||
class Elevation {
|
||||
Elevation._();
|
||||
|
||||
/// 시험용: 정해 두면 그 값
|
||||
@visibleForTesting
|
||||
static bool? debugElevated;
|
||||
|
||||
static bool? _cached;
|
||||
|
||||
/// 관리자 권한으로 실행 중인지 (Windows 만, 모르면 false)
|
||||
static bool get elevated {
|
||||
if (debugElevated case final v?) return v;
|
||||
if (!Platform.isWindows) return false;
|
||||
return _cached ??= () {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'dart:ffi';
|
|||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
|
|
@ -158,10 +157,6 @@ class SingleInstance {
|
|||
});
|
||||
}
|
||||
|
||||
/// 시험용: 받는 곳만 닫는다 (잠금 · instance.json 은 그대로 - 응답하지 않는 창)
|
||||
@visibleForTesting
|
||||
Future<void> debugStopListening() async => _server?.close();
|
||||
|
||||
/// 끝낼 때: instance.json (이 실행이 쓴 것일 때만) 을 지우고 잠금 파일을 놓는다
|
||||
Future<void> close() async {
|
||||
_timer?.cancel();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
/// 약속: 앱이 도는 기기가 무엇인지 (Android · Windows) 와 메모리를 아껴야 하는지 - 한곳에서만 정한다.
|
||||
/// 입력 없음 ([PlatformInfo.host] 는 이 기기를 그대로 읽는다), 출력 = 세 값, 오류 없음.
|
||||
/// 입력 없음 ([PlatformInfo.host] 는 이 기기를 그대로 읽는다), 출력 = 네 값, 오류 없음.
|
||||
/// 기기에 따라 다르게 도는 곳은 이것을 받아 (생성자 · 조립) 쓴다 - "Android 인 척" 하는 스위치를 따로 두지 않는다 (M1)
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../platform/windows/elevation.dart';
|
||||
|
||||
class PlatformInfo {
|
||||
final bool isAndroid;
|
||||
final bool isWindows;
|
||||
|
|
@ -12,10 +14,15 @@ class PlatformInfo {
|
|||
/// 메모리를 아끼는 방식 (모델을 나눠 열기 등). 처음 값은 Android
|
||||
final bool lowMemory;
|
||||
|
||||
const PlatformInfo({required this.isAndroid, required this.isWindows, bool? lowMemory}) : lowMemory = lowMemory ?? isAndroid;
|
||||
/// 관리자 권한 (높은 무결성) 으로 실행 중인지 (Windows 만 - 207, 내장 Chrome 엔진을 시작하지 않는 까닭)
|
||||
final bool elevated;
|
||||
|
||||
const PlatformInfo({required this.isAndroid, required this.isWindows, bool? lowMemory, this.elevated = false})
|
||||
: lowMemory = lowMemory ?? isAndroid;
|
||||
|
||||
/// 이 기기
|
||||
static final PlatformInfo host = PlatformInfo(isAndroid: Platform.isAndroid, isWindows: Platform.isWindows);
|
||||
static final PlatformInfo host =
|
||||
PlatformInfo(isAndroid: Platform.isAndroid, isWindows: Platform.isWindows, elevated: Elevation.elevated);
|
||||
|
||||
static const android = PlatformInfo(isAndroid: true, isWindows: false);
|
||||
static const windows = PlatformInfo(isAndroid: false, isWindows: true);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../platform/common/media_kit_player.dart';
|
|||
import '../app/ai_upscale.dart' show AiUpscaler;
|
||||
import '../app/settings.dart';
|
||||
import '../core/download_detect.dart';
|
||||
import '../core/lan_scan.dart' show LanHost;
|
||||
import '../platform/common/nllb_translator.dart';
|
||||
import '../platform/common/opensubtitles_provider.dart';
|
||||
import '../platform/common/whisper_recognizer.dart';
|
||||
|
|
@ -68,6 +69,9 @@ class PlatformServices {
|
|||
/// 엔진 · 모델을 이미 정해 둔 해상도 올리기 (없으면 설정대로 준비하고, 없는 엔진 · 모델은 그 자리에서 받는다)
|
||||
final AiUpscaler Function()? createUpscaler;
|
||||
|
||||
/// 같은 망의 SMB 서버 찾기 (없으면 이 기기의 사설망을 실제로 훑는다 - smb_settings)
|
||||
final Future<List<LanHost>> Function({bool Function()? cancelled, void Function(int done, int total)? onProgress})? findLanSmb;
|
||||
|
||||
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
||||
final PlatformInfo platform;
|
||||
|
||||
|
|
@ -84,6 +88,7 @@ class PlatformServices {
|
|||
this.createDownloadBackends,
|
||||
this.createSubtitleProviders,
|
||||
this.createUpscaler,
|
||||
this.findLanSmb,
|
||||
this.updater,
|
||||
this.usage,
|
||||
ModelStore? models,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import '../../core/download_detect.dart' show CookieExport, CookieRecord, isLogi
|
|||
import '../../core/web_address.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
import '../../platform/windows/cef_runtime.dart';
|
||||
import '../../platform/windows/elevation.dart';
|
||||
import '../cef_setup.dart' show confirmCefWhenElevated;
|
||||
import '../theme.dart';
|
||||
import 'edge_view.dart';
|
||||
|
|
@ -27,7 +26,7 @@ Widget browserEngineView(AppController c,
|
|||
return EdgeView(host: host, initialUrl: url, dataDir: c.settings.webViewDataDir, javaScript: b.javaScript);
|
||||
}
|
||||
// 207: 관리자 권한이면 처음 값으로 Chrome 엔진을 시작하지 않는다 (보안) - 이유와 고를 것
|
||||
if (Elevation.elevated && !b.cefWhenElevated) return CefElevatedBlocked(c: c);
|
||||
if (c.services.platform.elevated && !b.cefWhenElevated) return CefElevatedBlocked(c: c);
|
||||
// Chrome 엔진은 JavaScript 설정을 만든 뒤에 바꿀 수 없어 바뀌면 새로 만든다
|
||||
return CefView(
|
||||
key: ValueKey('${b.javaScript}|$cefTry'),
|
||||
|
|
@ -38,7 +37,8 @@ Widget browserEngineView(AppController c,
|
|||
// 207: 시작하지 못하면 사용자가 바로 고를 수 있게
|
||||
onUseEdge: () => c.updateSettings((x) => x.browser.engine = 'edge'),
|
||||
onRetry: onCefRetry,
|
||||
cefWhenElevated: b.cefWhenElevated);
|
||||
cefWhenElevated: b.cefWhenElevated,
|
||||
elevated: c.services.platform.elevated);
|
||||
}
|
||||
|
||||
/// 내장 Chrome 엔진 (환경 설정에서 고르고 내려받은 경우). 약속은 [EdgeView] 와 같다 (host 로 알리고 attach 로 조작을 준다):
|
||||
|
|
@ -51,6 +51,9 @@ class CefView extends StatefulWidget {
|
|||
final VoidCallback? onUseEdge;
|
||||
final VoidCallback? onRetry;
|
||||
final bool cefWhenElevated;
|
||||
|
||||
/// 관리자 권한으로 실행 중인지 (조립의 PlatformInfo)
|
||||
final bool elevated;
|
||||
const CefView(
|
||||
{super.key,
|
||||
required this.host,
|
||||
|
|
@ -59,11 +62,11 @@ class CefView extends StatefulWidget {
|
|||
required this.javaScript,
|
||||
this.onUseEdge,
|
||||
this.onRetry,
|
||||
this.cefWhenElevated = false});
|
||||
this.cefWhenElevated = false,
|
||||
this.elevated = false});
|
||||
|
||||
/// 207: 내장 Chrome 엔진이 이 시간 안에 준비되지 않으면 빙글이만 돌게 두지 않고 "시작하지 못했습니다" 와 고를 것을 보인다
|
||||
@visibleForTesting
|
||||
static Duration startTimeout = const Duration(seconds: 20);
|
||||
static const startTimeout = Duration(seconds: 20);
|
||||
|
||||
@override
|
||||
State<CefView> createState() => _CefViewState();
|
||||
|
|
@ -158,7 +161,7 @@ class _CefViewState extends State<CefView> {
|
|||
rootCachePath: widget.dataDir.isEmpty ? null : p.join(p.dirname(widget.dataDir), 'cef'),
|
||||
proxyServer: gate,
|
||||
// 207: 관리자 권한이면 Chromium 이 스스로 끝나므로, 사용자가 설정에서 고른 때만 그대로 돈다 (JJ_PATCH.md 7)
|
||||
doNotDeElevate: Elevation.elevated && widget.cefWhenElevated);
|
||||
doNotDeElevate: widget.elevated && widget.cefWhenElevated);
|
||||
await _cefStarted;
|
||||
_c.setWebviewListener(cef.WebviewEventsListener(
|
||||
onUrlChanged: (u) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import '../platform/windows/cef_runtime.dart';
|
|||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import 'tor_page.dart' show showTorNotice;
|
||||
import '../core/idle_stop.dart';
|
||||
|
||||
/// 249: DNS 서버에 닿지 않는 동안 보이는 줄 (환경 설정 > 네트워크 · DNS · 작업 현황) - 아래 안내를 놓쳐도 찾을 수 있게.
|
||||
/// 서버가 다시 답하거나 DNS 설정을 바꾸면 사라진다.
|
||||
|
|
@ -256,22 +257,26 @@ class _DnsSettingsState extends State<DnsSettings> {
|
|||
/// 환경 설정 > Tor 브라우저: 기록 남기기 · 홈 주소 · JavaScript · 안내 다시 보기 · 지금 지우기
|
||||
class TorSettings extends StatelessWidget {
|
||||
final AppController c;
|
||||
const TorSettings({super.key, required this.c});
|
||||
|
||||
/// 떠난 뒤 잠시 켜 두기 (처음 값 = 이 앱의 Tor)
|
||||
final IdleStop? idle;
|
||||
const TorSettings({super.key, required this.c, this.idle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = c.settings;
|
||||
final idle = this.idle ?? TorService.instance.idle;
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
// Tor 화면을 떠난 뒤 잠시 켜 두는 중이면 보인다 (언제 끄는지 · 지금 끄기)
|
||||
ValueListenableBuilder<DateTime?>(
|
||||
valueListenable: TorService.instance.idleStopAt,
|
||||
valueListenable: idle.stopAt,
|
||||
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('지금 끄기'))),
|
||||
trailing: OutlinedButton(onPressed: idle.stopNow, child: Text(tr('지금 끄기'))),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import '../../app/app_controller.dart';
|
||||
import '../../app/i18n_controller.dart' show I18nController;
|
||||
import '../../platform/windows/cef_runtime.dart';
|
||||
import '../../platform/windows/elevation.dart';
|
||||
import '../cef_setup.dart';
|
||||
import '../theme.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
|
|
@ -119,7 +118,7 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
|
|||
if (mounted) setState(() {});
|
||||
},
|
||||
title: Text(tr('관리자 권한일 때도 Chrome 엔진 쓰기 (권장하지 않음)')),
|
||||
subtitle: Text(Elevation.elevated
|
||||
subtitle: Text(c.services.platform.elevated
|
||||
? tr('지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.')
|
||||
: tr('관리자 권한으로 실행할 때만 쓰입니다. 끄면 (기본) 그때 Chrome 엔진을 시작하지 않고 Edge 를 권합니다.')),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
|
|||
icon: const Icon(Icons.radar, size: 18),
|
||||
label: Text(tr('같은 망에서 찾기')),
|
||||
onPressed: () async {
|
||||
final h = await pickLanSmbServer(ctx);
|
||||
final h = await pickLanSmbServer(ctx, c);
|
||||
if (h == null) return;
|
||||
set(() {
|
||||
host.text = h.name ?? h.address;
|
||||
|
|
@ -283,14 +283,10 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
|
|||
}
|
||||
|
||||
|
||||
/// 시험: 찾는 함수 (null 이면 실제로 훑는다)
|
||||
@visibleForTesting
|
||||
Future<List<LanHost>> Function({bool Function()? cancelled, void Function(int done, int total)? onProgress})? debugFindSmbServers;
|
||||
|
||||
/// X-plore j: 같은 망의 SMB 서버를 찾아 고르는 창 (진행 · 취소). 이 기기의 사설망 (/24) 의 445 포트에 연결만 해 보고 바로 닫는다 (보내는 것 없음)
|
||||
/// [find]: 시험에서 바꿔 끼움 (실제 망을 훑지 않게)
|
||||
Future<LanHost?> pickLanSmbServer(BuildContext context) async {
|
||||
final find = debugFindSmbServers ?? ({bool Function()? cancelled, void Function(int done, int total)? onProgress}) =>
|
||||
/// 찾기는 조립의 [PlatformServices.findLanSmb] (없으면 이 기기의 망을 실제로 훑는다)
|
||||
Future<LanHost?> pickLanSmbServer(BuildContext context, AppController c) async {
|
||||
final find = c.services.findLanSmb ?? ({bool Function()? cancelled, void Function(int done, int total)? onProgress}) =>
|
||||
findSmbServersBoth(findSmbServers, cancelled: cancelled, onProgress: onProgress);
|
||||
var cancelled = false;
|
||||
var done = 0, total = 0;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../app/app_controller.dart';
|
|||
import '../core/wifi_share.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
import '../core/wifi_share_net.dart';
|
||||
|
||||
/// 89: Wi-Fi 공유 화면 - 고른 폴더 하나를 같은 Wi-Fi 의 다른 기기 브라우저에서 보기 · 받기 (읽기 전용).
|
||||
/// 켜기 전: 폴더 · 저절로 끄는 시간 · 안내 · [켜기]. 켠 뒤: 6자리 코드 (크게) · 주소 · QR · 남은 시간 [+30분] · [끄기] · 접속 기록.
|
||||
|
|
@ -18,12 +19,10 @@ class WifiSharePage extends StatefulWidget {
|
|||
|
||||
/// 공유할 폴더 (이미 켜져 있으면 그 공유를 보인다)
|
||||
final String folder;
|
||||
const WifiSharePage({super.key, required this.c, required this.folder});
|
||||
|
||||
/// 시험: Windows 설정 · 제어판 열기를 바꾼다
|
||||
@visibleForTesting
|
||||
static Future<void> Function(String exe, List<String> args) debugLaunch =
|
||||
(exe, args) async => Process.start(exe, args, mode: ProcessStartMode.detached);
|
||||
/// 열 주소 · Windows 설정 · 제어판 열기 (처음 값 = 이 기기)
|
||||
final WifiShareNet net;
|
||||
const WifiSharePage({super.key, required this.c, required this.folder, this.net = const RealWifiShareNet()});
|
||||
|
||||
static Future<void> open(BuildContext context, AppController c, String folder) =>
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => WifiSharePage(c: c, folder: folder)));
|
||||
|
|
@ -76,7 +75,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
try {
|
||||
final mins = widget.c.settings.tools.wifiShareMinutes;
|
||||
final s = await WifiShare.start(widget.folder,
|
||||
autoOff: mins > 0 ? Duration(minutes: mins) : null, tailscale: widget.c.settings.tools.wifiShareTailscale);
|
||||
autoOff: mins > 0 ? Duration(minutes: mins) : null, tailscale: widget.c.settings.tools.wifiShareTailscale, net: widget.net);
|
||||
await widget.c.updateSettings((x) => x.tools.wifiShareFolder = widget.folder);
|
||||
_s?.removeListener(_changed);
|
||||
_s = s..addListener(_changed);
|
||||
|
|
@ -247,7 +246,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 26),
|
||||
child: TextButton.icon(
|
||||
onPressed: () => WifiSharePage.debugLaunch('explorer.exe', ['ms-settings:network-status']),
|
||||
onPressed: () => widget.net.launch('explorer.exe', ['ms-settings:network-status']),
|
||||
icon: const Icon(Icons.open_in_new, size: 16),
|
||||
label: Text(tr('네트워크 설정 열기')),
|
||||
),
|
||||
|
|
@ -257,7 +256,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 26),
|
||||
child: TextButton.icon(
|
||||
onPressed: () => WifiSharePage.debugLaunch('control.exe', ['/name', 'Microsoft.WindowsFirewall', '/page', 'pageConfigureApps']),
|
||||
onPressed: () => widget.net.launch('control.exe', ['/name', 'Microsoft.WindowsFirewall', '/page', 'pageConfigureApps']),
|
||||
icon: const Icon(Icons.open_in_new, size: 16),
|
||||
label: Text(tr('방화벽 허용 앱 열기')),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import 'package:jj_mkvmaker/app/bookmarks_controller.dart';
|
|||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/cef_runtime.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/elevation.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/app_actions.dart';
|
||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||
|
|
@ -21,9 +21,15 @@ void main() {
|
|||
late Directory dir;
|
||||
late AppController c;
|
||||
late BookmarksController bm;
|
||||
|
||||
/// 관리자 권한 여부는 조립 (PlatformInfo) 으로 넘긴다
|
||||
AppController controller({required bool elevated}) => AppController(PlatformServices(
|
||||
mediaTool: ProcessMediaTool('x', 'y'),
|
||||
storage: DesktopStorageService(),
|
||||
platform: PlatformInfo(isAndroid: false, isWindows: true, elevated: elevated)));
|
||||
setUp(() {
|
||||
dir = Directory.systemTemp.createTempSync('jj_cef_elev_');
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c = controller(elevated: false);
|
||||
c.settings
|
||||
..browser.engine = 'chrome'
|
||||
..webViewDataDir = p.join(dir.path, 'webview');
|
||||
|
|
@ -31,7 +37,6 @@ void main() {
|
|||
CefRuntime.readyThisRun = true;
|
||||
});
|
||||
tearDown(() {
|
||||
Elevation.debugElevated = null;
|
||||
CefRuntime.readyThisRun = false;
|
||||
dir.deleteSync(recursive: true);
|
||||
});
|
||||
|
|
@ -48,7 +53,7 @@ void main() {
|
|||
}
|
||||
|
||||
testWidgets('관리자 권한: Chrome 엔진을 시작하지 않고 이유 · [Edge 엔진으로 바꾸기]', (t) async {
|
||||
Elevation.debugElevated = true;
|
||||
c = controller(elevated: true)..settings = c.settings;
|
||||
await open(t);
|
||||
expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsOneWidget);
|
||||
await t.tap(find.text('Edge 엔진으로 바꾸기'));
|
||||
|
|
@ -58,7 +63,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('관리자 권한: [그래도 Chrome 엔진 쓰기…] 는 경고를 보이고, 취소하면 그대로 · 고르면 설정이 켜짐', (t) async {
|
||||
Elevation.debugElevated = true;
|
||||
c = controller(elevated: true)..settings = c.settings;
|
||||
await open(t);
|
||||
await t.tap(find.text('그래도 Chrome 엔진 쓰기…'));
|
||||
await t.pumpAndSettle();
|
||||
|
|
@ -76,13 +81,10 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('시작하지 못하면 빙글이만 돌지 않고 "Chrome 엔진을 시작하지 못했습니다" · [Edge 엔진으로 바꾸기] · [다시 시도]', (t) async {
|
||||
Elevation.debugElevated = false;
|
||||
CefView.startTimeout = const Duration(seconds: 2);
|
||||
addTearDown(() => CefView.startTimeout = const Duration(seconds: 20));
|
||||
await open(t);
|
||||
// 시험에는 Chrome 엔진 (플러그인) 이 없어 끝나지 않거나 실패한다 - 정한 시간 (여기선 2초) 이 지나면 안내
|
||||
// 시험에는 Chrome 엔진 (플러그인) 이 없어 끝나지 않거나 실패한다 - 정한 시간 (20초) 이 지나면 안내 (가짜 시간을 넘김)
|
||||
expect(find.text('Chrome 엔진을 시작하지 못했습니다'), findsNothing);
|
||||
await t.pump(const Duration(seconds: 3));
|
||||
await t.pump(CefView.startTimeout + const Duration(seconds: 1));
|
||||
expect(find.text('Chrome 엔진을 시작하지 못했습니다'), findsOneWidget);
|
||||
expect(find.text('다시 시도'), findsOneWidget);
|
||||
await t.tap(find.text('Edge 엔진으로 바꾸기'));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import 'package:jj_mkvmaker/core/vfs.dart';
|
|||
import 'package:jj_mkvmaker/core/wifi_share.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/loopback_net.dart';
|
||||
|
||||
/// 새 네트워크 기능 보안 검토 (관리자 지시 10/18) 의 고침:
|
||||
/// 1 원격 이름으로 로컬 경로 벗어나기 · 3 Wi-Fi 공유는 Wi-Fi · 유선 주소에만 · 4 SMB 찾기는 Wi-Fi · 유선만 · 11 mDNS 는 사설망 주소만
|
||||
void main() {
|
||||
|
|
@ -72,18 +74,14 @@ void main() {
|
|||
test('3: Wi-Fi · 유선 주소가 없으면 공유를 켜지 않는다 (모든 연결 0.0.0.0 에 열지 않음)', () async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_netsec_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
WifiShare.debugAddresses = const [];
|
||||
addTearDown(() => WifiShare.debugAddresses = null);
|
||||
await expectLater(WifiShare.start(tmp.path), throwsA(isA<SocketException>()));
|
||||
await expectLater(WifiShare.start(tmp.path, net: LoopbackNet(const [])), throwsA(isA<SocketException>()));
|
||||
expect(WifiShare.instance, isNull);
|
||||
});
|
||||
|
||||
test('3: 공유는 고른 주소에만 열린다 (127.0.0.1 로 열면 다른 주소로는 닿지 않음)', () async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_netsec_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
WifiShare.debugAddresses = const ['127.0.0.1'];
|
||||
addTearDown(() => WifiShare.debugAddresses = null);
|
||||
final s = await WifiShare.start(tmp.path, port: 0);
|
||||
final s = await WifiShare.start(tmp.path, port: 0, net: LoopbackNet());
|
||||
addTearDown(s.stop);
|
||||
expect(s.addresses, ['127.0.0.1']);
|
||||
final ok = await Socket.connect('127.0.0.1', s.port);
|
||||
|
|
|
|||
|
|
@ -249,8 +249,12 @@ void main() {
|
|||
test('첫 창이 응답하지 않으면 unresponsive (묻기 - 같은 데이터 위에 창 두 개를 막는다)', () async {
|
||||
final holder = SingleInstance(dataDir: d1.path);
|
||||
expect(await holder.claim(const LaunchRequest(LaunchAction.play, [])), ClaimResult.first);
|
||||
// 응답하지 않는 창: 받는 곳을 닫았다
|
||||
await holder.debugStopListening();
|
||||
// 응답하지 않는 창: instance.json 의 포트에 아무도 듣지 않는다 (첫 창의 받는 곳이 죽음 - 잠금은 그대로)
|
||||
final gone = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final port = gone.port;
|
||||
await gone.close();
|
||||
final info = File(p.join(d1.path, 'instance.json'));
|
||||
info.writeAsStringSync(jsonEncode({...jsonDecode(info.readAsStringSync()) as Map, 'port': port}));
|
||||
expect(await SingleInstance(dataDir: d1.path).claim(const LaunchRequest(LaunchAction.play, [])),
|
||||
ClaimResult.unresponsive);
|
||||
await holder.close();
|
||||
|
|
|
|||
|
|
@ -9,20 +9,18 @@ import 'package:jj_mkvmaker/ui/smb_settings.dart';
|
|||
|
||||
/// X-plore j: [SMB 서버 추가] 창의 [같은 망에서 찾기] - 진행 · 찾은 서버를 고르면 주소 (이름) 칸을 채움. 시험은 실제 망을 훑지 않는다 (가짜 찾기)
|
||||
void main() {
|
||||
tearDown(() => debugFindSmbServers = null);
|
||||
|
||||
testWidgets('찾기 → 고르면 주소 · 이름 칸 채움, 못 찾으면 안내', (tester) async {
|
||||
tester.view.physicalSize = const Size(1000, 1200);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
var calls = 0;
|
||||
var result = const [LanHost('192.168.0.9', 'nas.local'), LanHost('192.168.0.20', null)];
|
||||
debugFindSmbServers = ({cancelled, onProgress}) async {
|
||||
Future<List<LanHost>> findLan({cancelled, onProgress}) async {
|
||||
calls++;
|
||||
onProgress?.call(254, 254);
|
||||
return result;
|
||||
};
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
}
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), findLanSmb: findLan));
|
||||
final key = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: key, home: const Scaffold()));
|
||||
editSmbServer(key.currentContext!, c);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import 'package:jj_mkvmaker/core/wifi_share_net.dart';
|
||||
|
||||
/// 시험용 Wi-Fi 공유 망: 정한 주소에만 연다 (처음 값 127.0.0.1 - 시험 PC 의 방화벽 창 · 바깥 노출 없이),
|
||||
/// Windows 설정 · 제어판은 열지 않고 적어 둔다 (이 PC 의 앱을 열지 않게). 열기 자체는 실제 서버 그대로
|
||||
class LoopbackNet implements WifiShareNet {
|
||||
LoopbackNet([this.addrs = const ['127.0.0.1']]);
|
||||
final List<String> addrs;
|
||||
final launched = <String>[];
|
||||
|
||||
@override
|
||||
Future<List<String>> addresses({bool tailscale = true}) async => addrs;
|
||||
@override
|
||||
Future<void> launch(String exe, List<String> args) async => launched.add('$exe ${args.join(' ')}');
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ 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';
|
||||
import 'package:jj_mkvmaker/core/idle_stop.dart';
|
||||
|
||||
void main() {
|
||||
test('tor 알림 줄 읽기 · torrc', () {
|
||||
|
|
@ -288,30 +289,38 @@ void main() {
|
|||
expect(find.textContaining('VIEW'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('떠난 뒤 tor 는 정한 시간 뒤에 끈다 · 돌아오면 그대로 · 0 은 바로 (가짜 시간)', (t) async {
|
||||
final s = TorService.instance;
|
||||
addTearDown(() => s.debugRunning = false);
|
||||
s.debugRunning = true;
|
||||
s.status.value = const TorStatus(TorPhase.ready, percent: 100);
|
||||
await s.stopAfter(const Duration(minutes: 2));
|
||||
expect(s.idleStopAt.value, isNotNull);
|
||||
testWidgets('떠난 뒤 tor 는 정한 시간 뒤에 끈다 · 돌아오면 그대로 · 0 은 바로 (가짜 시간, IdleStop 약속)', (t) async {
|
||||
var stopped = 0;
|
||||
final idle = IdleStop(() async => stopped++);
|
||||
await idle.schedule(const Duration(minutes: 2));
|
||||
expect(idle.stopAt.value, isNotNull);
|
||||
await t.pump(const Duration(minutes: 1, seconds: 59));
|
||||
expect(s.running, isTrue);
|
||||
expect(stopped, 0);
|
||||
await t.pump(const Duration(seconds: 2));
|
||||
expect(s.running, isFalse);
|
||||
expect(s.idleStopAt.value, isNull);
|
||||
expect(s.status.value.phase, TorPhase.off);
|
||||
expect([stopped, idle.stopAt.value], [1, null]);
|
||||
// 그 사이 돌아오면 그대로 켜 둔다
|
||||
s.debugRunning = true;
|
||||
s.status.value = const TorStatus(TorPhase.ready, percent: 100);
|
||||
await s.stopAfter(const Duration(minutes: 2));
|
||||
await idle.schedule(const Duration(minutes: 2));
|
||||
await t.pump(const Duration(minutes: 1));
|
||||
s.cancelIdleStop();
|
||||
idle.cancel();
|
||||
await t.pump(const Duration(minutes: 5));
|
||||
expect(s.running, isTrue);
|
||||
expect([stopped, idle.stopAt.value], [1, null]);
|
||||
// 다시 정하면 앞의 것은 잊는다
|
||||
await idle.schedule(const Duration(minutes: 1));
|
||||
await idle.schedule(const Duration(minutes: 3));
|
||||
await t.pump(const Duration(minutes: 2));
|
||||
expect(stopped, 1);
|
||||
await t.pump(const Duration(minutes: 1, seconds: 1));
|
||||
expect(stopped, 2);
|
||||
// 0 = 바로
|
||||
await s.stopAfter(Duration.zero);
|
||||
await idle.schedule(Duration.zero);
|
||||
expect([stopped, idle.stopAt.value], [3, null]);
|
||||
});
|
||||
|
||||
test('tor 가 떠 있지 않으면 stopAfter 는 아무것도 하지 않는다', () async {
|
||||
final s = TorService.instance;
|
||||
expect(s.running, isFalse);
|
||||
await s.stopAfter(const Duration(minutes: 2));
|
||||
expect(s.idleStopAt.value, isNull);
|
||||
});
|
||||
|
||||
test('설정: Tor 를 켜 둘 시간 (처음 2분, 틀린 값은 2분)', () {
|
||||
|
|
@ -347,20 +356,17 @@ void main() {
|
|||
|
||||
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)))));
|
||||
var stopped = 0;
|
||||
final idle = IdleStop(() async => stopped++);
|
||||
addTearDown(idle.cancel);
|
||||
await t.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: TorSettings(c: c, idle: idle)))));
|
||||
expect(find.textContaining('Tor 대기 중'), findsNothing);
|
||||
s.debugRunning = true;
|
||||
await s.stopAfter(const Duration(minutes: 2));
|
||||
await idle.schedule(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(stopped, 1);
|
||||
expect(find.textContaining('Tor 대기 중'), findsNothing);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@ import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
|||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/wifi_share_page.dart';
|
||||
|
||||
import 'support/loopback_net.dart';
|
||||
|
||||
/// 89: Wi-Fi 공유 화면 - 처음엔 꺼짐 · [켜기] 뒤 6자리 코드 · 주소 · 남은 시간 · [끄기] 로 꺼짐. 설정 파일은 쓰지 않는다 (저장소 없음)
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
|
||||
setUp(() {
|
||||
WifiShare.debugLoopbackOnly = true;
|
||||
tmp = Directory.systemTemp.createTempSync('jj_wifipage_');
|
||||
File('${tmp.path}${Platform.pathSeparator}a.txt').writeAsStringSync('a');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await WifiShare.instance?.stop();
|
||||
WifiShare.debugLoopbackOnly = false;
|
||||
try {
|
||||
tmp.deleteSync(recursive: true);
|
||||
} catch (_) {}
|
||||
|
|
@ -34,7 +34,7 @@ void main() {
|
|||
tester.view.physicalSize = const Size(412, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(MaterialApp(home: WifiSharePage(c: c, folder: tmp.path)));
|
||||
await tester.pumpWidget(MaterialApp(home: WifiSharePage(c: c, folder: tmp.path, net: LoopbackNet())));
|
||||
expect(find.text('켜기'), findsOneWidget);
|
||||
expect(find.text('30분 뒤'), findsOneWidget);
|
||||
expect(WifiShare.instance, isNull, reason: '화면을 열기만 해서는 켜지지 않는다 (기본 꺼짐)');
|
||||
|
|
@ -59,15 +59,12 @@ void main() {
|
|||
});
|
||||
testWidgets('Windows: 켜기 전에 방화벽 안내 · 펼치면 "개인 네트워크 허용" 하는 법과 설정 · 제어판을 여는 버튼 (관리자 지시 10/19)', (tester) async {
|
||||
if (!Platform.isWindows) return;
|
||||
final launched = <String>[];
|
||||
final old = WifiSharePage.debugLaunch;
|
||||
WifiSharePage.debugLaunch = (exe, args) async => launched.add('$exe ${args.join(' ')}');
|
||||
addTearDown(() => WifiSharePage.debugLaunch = old);
|
||||
final net = LoopbackNet();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
tester.view.physicalSize = const Size(412, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(MaterialApp(home: WifiSharePage(c: c, folder: tmp.path)));
|
||||
await tester.pumpWidget(MaterialApp(home: WifiSharePage(c: c, folder: tmp.path, net: net)));
|
||||
expect(find.textContaining('"개인 네트워크" 에 체크하고 [허용]'), findsOneWidget, reason: '켜기 전에 미리');
|
||||
expect(find.text('방화벽 허용 앱 열기'), findsNothing, reason: '도움말은 접혀 있음');
|
||||
await tester.tap(find.text('다른 기기에서 열리지 않으면 (Windows 방화벽)'));
|
||||
|
|
@ -76,6 +73,6 @@ void main() {
|
|||
await tester.tap(find.text('네트워크 설정 열기'));
|
||||
await tester.tap(find.text('방화벽 허용 앱 열기'));
|
||||
await tester.pump();
|
||||
expect(launched, ['explorer.exe ms-settings:network-status', 'control.exe /name Microsoft.WindowsFirewall /page pageConfigureApps']);
|
||||
expect(net.launched, ['explorer.exe ms-settings:network-status', 'control.exe /name Microsoft.WindowsFirewall /page pageConfigureApps']);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:jj_mkvmaker/core/wifi_share.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/loopback_net.dart';
|
||||
|
||||
/// 89: Wi-Fi 공유 서버 - 코드 없으면 401 · 틀리면 막기 · 폴더 밖 · 링크 403 · 읽기만 (405) · Range · 격리 · 저절로 끄기
|
||||
void main() {
|
||||
late Directory tmp, shared;
|
||||
|
|
@ -21,7 +23,7 @@ void main() {
|
|||
Directory(p.join(shared.path, 'sub')).createSync();
|
||||
File(p.join(shared.path, 'sub', 'b.bin')).writeAsBytesSync([1, 2, 3]);
|
||||
File(p.join(tmp.path, 'secret.txt')).writeAsStringSync('밖의 파일');
|
||||
s = await WifiShare.start(shared.path, port: 0, random: Random(7), loopbackOnly: true);
|
||||
s = await WifiShare.start(shared.path, port: 0, random: Random(7), net: LoopbackNet());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
|
@ -45,6 +47,18 @@ void main() {
|
|||
|
||||
Future<String> body(HttpClientResponse r) => r.transform(utf8.decoder).join();
|
||||
|
||||
/// [source] 주소에서 틀린 코드로 한 번 (실제 연결 - 서버가 보는 상대 주소가 [source])
|
||||
Future<void> wrongFrom(String source, String wrong) async {
|
||||
final so = await Socket.connect('127.0.0.1', s.port, sourceAddress: source);
|
||||
so.write('GET /?code=$wrong HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n');
|
||||
try {
|
||||
await so.cast<List<int>>().transform(latin1.decoder).join();
|
||||
} on SocketException {
|
||||
// 30번째에서 공유가 꺼지며 끊길 수 있다
|
||||
}
|
||||
so.destroy();
|
||||
}
|
||||
|
||||
Future<String> login() async {
|
||||
final r = await get('/?code=${s.code}');
|
||||
expect(r.statusCode, 303);
|
||||
|
|
@ -164,9 +178,9 @@ void main() {
|
|||
var r = await get('/?code=${s.code}');
|
||||
expect(r.statusCode, 429, reason: '막힌 동안은 맞는 코드도 안 됨');
|
||||
await r.drain<void>();
|
||||
// 모두 합쳐 30번 (막힌 동안은 세지 않으므로 시험에서 바로 센다)
|
||||
// 모두 합쳐 30번: 다른 기기들처럼 다른 루프백 주소 (127.0.0.x) 에서 실제로 틀린 코드를 보낸다 (주소마다 막히기 전까지 4번)
|
||||
for (var i = WifiShare.perClientFails; i < WifiShare.totalFails; i++) {
|
||||
s.debugFail('10.0.0.${i % 200}');
|
||||
await wrongFrom('127.0.0.${2 + i ~/ 4}', wrong);
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
expect(s.running, isFalse);
|
||||
|
|
@ -185,7 +199,7 @@ void main() {
|
|||
expect(s.log.map((x) => x.path), contains('/a.txt'));
|
||||
expect(s.log.any((x) => x.path.contains(s.code)), isFalse);
|
||||
await s.stop();
|
||||
final t = await WifiShare.start(shared.path, port: 0, autoOff: const Duration(milliseconds: 200), loopbackOnly: true);
|
||||
final t = await WifiShare.start(shared.path, port: 0, autoOff: const Duration(milliseconds: 200), net: LoopbackNet());
|
||||
expect(t.endsAt, isNotNull);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
expect(t.running, isFalse);
|
||||
|
|
|
|||
Loading…
Reference in New Issue