import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; /// 88: SFTP 시험 서버 (tool/sftp_test_server.py, paramiko 3.5.1). 파이썬은 JJ_SFTP_PY, 없으면 ..\tools\sftp_test_py 의 venv final sftpTestPython = Platform.environment['JJ_SFTP_PY'] ?? p.normalize(p.join(Directory.current.path, '..', 'tools', 'sftp_test_py', Platform.isWindows ? 'Scripts/python.exe' : 'bin/python')); /// 시험 서버를 띄울 수 있는지 (없으면 그 시험은 건너뛴다) bool get sftpTestServerAvailable => File(sftpTestPython).existsSync(); class SftpTestServer { final Process process; final int port; /// 서버 호스트 키의 지문 ("SHA256:…") final String fingerprint; SftpTestServer._(this.process, this.port, this.fingerprint); /// [root] 폴더를 내놓는 서버. [hostKeyFile] 이 없으면 만든다 (같은 파일이면 같은 키) static Future start({ required String root, required String hostKeyFile, String user = 'jj', String? password = 'pw', String? pubkey, String keytype = 'ed25519', bool legacyOnly = false, // 캡처 시험 (관리자 10/24): 포트 · 호스트 키를 실행마다 같게 (창의 포트 칸 · 지문 글이 흔들리지 않게). 0 이면 빈 포트 int fixedPort = 0, bool fixedHostKey = false, }) async { final pr = await Process.start(sftpTestPython, [ 'tool/sftp_test_server.py', '--root', root, '--hostkey', hostKeyFile, '--keytype', keytype, '--user', user, if (password != null) ...['--password', password], if (pubkey != null) ...['--pubkey', pubkey], if (legacyOnly) '--legacy-only', if (fixedPort != 0) ...['--port', '$fixedPort'], if (fixedHostKey) '--fixed-hostkey', ]); final lines = pr.stdout.transform(utf8.decoder).transform(const LineSplitter()).asBroadcastStream(); pr.stderr.drain(); final port = int.parse((await lines.firstWhere((l) => l.startsWith('PORT ')).timeout(const Duration(seconds: 60))).split(' ')[1]); final fp = (await lines.firstWhere((l) => l.startsWith('FINGERPRINT ')).timeout(const Duration(seconds: 30))).split(' ')[1]; // 무거울 때도 포트가 연결을 받는 것을 본 뒤에 시작한다 (관리자 지적 10/18) final end = DateTime.now().add(const Duration(seconds: 30)); while (true) { try { (await Socket.connect('127.0.0.1', port, timeout: const Duration(seconds: 2))).destroy(); break; } on SocketException { if (DateTime.now().isAfter(end)) rethrow; await Future.delayed(const Duration(milliseconds: 200)); } } return SftpTestServer._(pr, port, fp); } /// 끝내기 (표준 입력을 닫으면 서버가 끝난다) Future stop() async { await process.stdin.close().catchError((_) {}); process.kill(); await process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () => -1); } }