diff --git a/AGENTS.md b/AGENTS.md index 86def2f..de80da6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,8 +21,12 @@ approval before operating Tornado2/PGM. The standing authorization includes launching and closing the development app, CONNECT/DISCONNECT, PREPARE, TAKE IN, NEXT, Page NEXT, TAKE OUT, observing Network Monitoring/PGM, and development-DB reads or writes needed by an in-scope migration -test. Prefer the existing Debug development-live profile and its ignored local -authorization file; do not weaken Release/default `DryRun` behavior. +test. On designated playout PCs, normal Debug and Release application launches are Live. +Never use DryRun for app launches, tests, diagnostics, or migration validation. +Every in-scope playout run on a designated development playout PC must use Live +output. If required DB, K3D, target identity, allowlist, or authorization +validation fails, block startup with a clear error instead of falling back to +DryRun. This is standing permission, not permission to guess after an ambiguous outcome: diff --git a/Config/playout.example.json b/Config/playout.example.json index ba93de0..f1a8689 100644 --- a/Config/playout.example.json +++ b/Config/playout.example.json @@ -1,5 +1,5 @@ { - "mode": "DryRun", + "mode": "Live", "host": "127.0.0.1", "port": 30001, "tcpMode": 1, diff --git a/README.md b/README.md index e2a8bb6..38945f4 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,10 @@ - WebView 코스피·코스닥·NXT·5개 지수·해외 실데이터 조회와 장애 UI - COM 중립 `IPlayoutEngine`, x64 K3D late binding, 전용 STA 큐와 프로세스 감시 - 네이티브 결과가 성공한 뒤에만 갱신되는 `PREPARE` / `TAKE IN` / `NEXT` / `TAKE OUT` WebView 브리지 -- 기본 `DryRun`, Test 전용 인스턴스·채널·씬 allowlist 및 Live 이중 승인 안전 게이트 +- 지정 개발 PC의 보호된 Live 전용 기동, Test 전용 인스턴스·채널·씬 allowlist 및 Live 이중 승인 안전 게이트 - MSIX 패키지 매니페스트와 x64 게시 프로필 -Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑터와 35개 scene 계약을 구현했습니다. 2026-07-22 현재 최신 개발 MSIX의 전체 UI/DB·로컬 상태 물리 검증과 실제 PGM runtime route code 33/34가 승인된 action 범위에서 끝났습니다. `s5006` 큐브 배경 action과 `s6001` 국가 영상 13 action만 원본에도 없는 외부 제작 자산 때문에 fail closed 상태이며, `s6001`의 두바이유·WTI·브렌트유·금 4 action은 실제 PGM 검증을 완료했습니다. 앱과 Release 패키지의 기본 모드는 계속 `DryRun`이며 Live allowlist, 명령 예산, callback/`OutcomeUnknown` 게이트를 완화하지 않습니다. 이 개발 PC의 Tornado2/PGM 마이그레이션 검증은 사용자가 반복 승인 없이 진행하도록 상시 허용했으며, 적용 범위와 중단 조건은 [개발 환경 상시 권한](docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md)에 기록했습니다. DB 설정은 [DB 운영 가이드](docs/DATABASE.md), 송출 설정·실제 검증 증거·롤백은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md), 원본 Scene/PageN 대조는 [송출 흐름 분석](docs/LEGACY_PLAYOUT_ANALYSIS.md), 장면별 현황은 [35개 Scene 동등성 매트릭스](docs/SCENE_EQUIVALENCE.md), 전체 412개 실행 action과 전용 편집 화면은 [운영자 UI 동등성 인벤토리](docs/OPERATOR_UI_PARITY.md)를 참고하세요. +Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑터와 35개 scene 계약을 구현했습니다. 2026-07-22 현재 최신 개발 MSIX의 전체 UI/DB·로컬 상태 물리 검증과 실제 PGM runtime route code 33/34가 승인된 action 범위에서 끝났습니다. `s5006` 큐브 배경 action과 `s6001` 국가 영상 13 action만 원본에도 없는 외부 제작 자산 때문에 fail closed 상태이며, `s6001`의 두바이유·WTI·브렌트유·금 4 action은 실제 PGM 검증을 완료했습니다. 지정 개발 PC의 Debug·Release 패키지는 보호된 `Live` 설정으로만 기동하며 `DryRun`으로 폴백하지 않습니다. Live allowlist, 명령 예산, callback/`OutcomeUnknown` 게이트는 완화하지 않습니다. 이 개발 PC의 Tornado2/PGM 마이그레이션 검증은 사용자가 반복 승인 없이 진행하도록 상시 허용했으며, 적용 범위와 중단 조건은 [개발 환경 상시 권한](docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md)에 기록했습니다. DB 설정은 [DB 운영 가이드](docs/DATABASE.md), 송출 설정·실제 검증 증거·롤백은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md), 원본 Scene/PageN 대조는 [송출 흐름 분석](docs/LEGACY_PLAYOUT_ANALYSIS.md), 장면별 현황은 [35개 Scene 동등성 매트릭스](docs/SCENE_EQUIVALENCE.md), 전체 412개 실행 action과 전용 편집 화면은 [운영자 UI 동등성 인벤토리](docs/OPERATOR_UI_PARITY.md)를 참고하세요. 35개 Scene/PageN 런타임과 원본 MainForm·UC1~UC7·GraphE·FSell·VIList·PList·AList·ThemeA·EList의 화면·bridge 계약은 WinUI 3/WebView2에 연결했습니다. 최신 패키지에서 실제 Windows 입력 1,838건으로 검색·키보드·컷/플레이리스트 drag·PList 모달과 화면별 버튼, 개발 DB create/save→fresh readback→delete를 검증했고 cleanup까지 확인했습니다. PList 2,213행은 모두 목록과 scene alias를 복원하며, 그중 1,617행은 fresh typed selection까지 증명됐고 596행은 식별정보 부족으로 추정 없이 보류합니다. `5077` 6행 20페이지 전체, 마지막 페이지, `5088` 12행 전환, 장기 실행 뒤 unload와 `s6001` 영상 비의존 4 action도 실제 PGM에서 확인했습니다. 고객 배포용 서명 MSIX, 외부 영상·배경 자산, 보류 596행의 DBA 결정은 아직 남아 있습니다. @@ -71,7 +71,7 @@ Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑 새로 받은 clone에 아직 이 PC의 검증된 runtime 연결이 없으면, 주변에 과거 `MBN_STOCK_N\...\bin\Debug`가 있더라도 첫 F5는 안전한 **소스 전용 첫 실행 설정 앱**으로 빌드됩니다. 이 첫 프로세스는 과거 로컬 송출 프로필·환경 -변수·Development Live 실행 인수를 무시하고 DB 미연결과 DryRun을 강제하므로 설정 중에는 +변수·Development Live 실행 인수를 무시하고 DB와 송출 엔진을 만들지 않으므로 설정 중에는 Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중간 패키징 파일도 이전 전체 빌드의 `Cuts`, `Res` 또는 DB 설정을 잘못 재사용하지 않도록 `bin\SourceOnly`와 `obj\SourceOnly`에 분리됩니다. @@ -96,19 +96,19 @@ Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중 실행하지 않으므로 즉시 완료되어야 합니다. 전체 앱 빌드는 창을 닫고 누르는 다음 F5에서 진행됩니다. 이때도 `Cuts`는 복사하지 않고 선택한 원래 경로를 직접 사용합니다. -Development Live용 DB overlay, K3D pin과 한 회차 승인 파일이 아직 없는 PC는 별도의 -`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 안전한 `DryRun`으로 -시작합니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다. +Development Live용 DB overlay, K3D pin과 승인 파일이 아직 없는 PC는 별도의 +`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 전체 앱 기동이 +차단됩니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다. 초기화가 성공하면 Visual Studio에서 `Debug|x64`, `Legacy Parity App (VS F5)`, `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작합니다. -앱이나 빌드 출력이 `SourceOnly` 또는 `DryRun`이라고 표시되면 송출을 시도하지 말고 별도 +앱이나 빌드 출력이 `SourceOnly` 또는 Live 기동 차단이라고 표시되면 송출을 시도하지 말고 Development Live 준비 상태를 확인합니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라 -`5001` 하나로 진행합니다. Development Live는 이 Debug 프로세스에만 적용되며 Release와 기본 -프로필은 계속 `DryRun`입니다. 이 프로필은 보호된 LocalAppData 설정만 읽고 상속된 +`5001` 하나로 진행합니다. Debug와 Release의 일반 실행은 모두 같은 보호된 Live +부트스트랩을 사용합니다. 이 프로필은 보호된 LocalAppData 설정만 읽고 상속된 `MBN_STOCK_PLAYOUT_*`·DB 환경 변수를 무시합니다. scene root는 첫 실행에서 저장한 원래 `Cuts` 경로로 고정하며 strict -검증이 실패하면 Live를 승인하지 않고 안전한 DryRun으로 시작합니다. +검증이 실패하면 Live 엔진을 만들지 않고 명확한 기동 오류를 표시합니다. 같은 clone에서는 로컬 연결 파일이 Git에 덮어써지지 않으므로 이후에는 `git pull` 후 F5로 확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB 폴더 연결이 바뀌면 첫 실행 @@ -140,20 +140,20 @@ dotnet restore MBN_STOCK_WEBVIEW.sln -p:Platform=x64 dotnet build MBN_STOCK_WEBVIEW.sln -c Debug -p:Platform=x64 ``` -K3D 등록 상태와 COM을 열지 않는 dry-run 송출 흐름 확인: +K3D 등록 상태를 COM 활성화 없이 확인: ```powershell powershell -NoProfile -ExecutionPolicy Bypass ` -File .\scripts\Inspect-K3DRegistration.ps1 dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke ` - -c Debug -p:Platform=x64 -- --dry-run + -c Debug -p:Platform=x64 -- --probe ``` 격리 Tornado TEST의 프로세스·Network Server 포트·loopback 주소·LISTEN 소유권은 COM을 열기 전에 [읽기 전용 점검 스크립트](scripts/Inspect-TornadoTestIsolation.ps1)로 확인합니다. 설치 폴더의 다른 Tornado2 버전 EXE는 현재 사용자 설정과 출력 장치를 공유할 수 있으므로 PGM 옆에서 TEST 대용으로 실행하지 않습니다. 별도 Test 인스턴스 검증은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md)의 단계별 CLI를 사용합니다. `--test-plan`은 절대 경로 로컬 JSON과 씬 자산만 확인하며 엔진/COM을 만들지 않습니다. PGM/PROGRAM이 전혀 없는 격리 출력에서만 `--test-connect`로 연결·해제를 확인한 뒤 `--test-sequence`로 `PREPARE → TAKE IN → NEXT → TAKE OUT`을 실행합니다. Test 명령은 `MBN_STOCK_PLAYOUT_*` 환경 override와 Live 설정을 거부하고 자동 재연결·재생을 하지 않습니다. 승인된 Test 씬 후보는 basename `5001`, `5006`이며 실제 자산 경로는 저장소에 기록하지 않습니다. -중요: 앱의 기본 `DryRun`, `--probe`, `--dry-run`, `--test-plan`은 `KTAPConnect`를 호출하지 않습니다. 따라서 이 단계에서 Tornado2의 `View > Network Monitoring Window`에 앱이 보낸 통신 기록이 없는 것은 정상입니다. 이 CLI 절차에서는 모든 안전 게이트를 통과한 격리 `Test`의 `--test-connect`에서 처음 네트워크 기록을 기대하며, 별도로 승인된 UI Test 실행도 기록을 만들 수 있습니다. 사용할 포트는 예제 숫자가 아니라 해당 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port` 실값이어야 합니다. +중요: `--probe`와 `--test-plan`은 `KTAPConnect`를 호출하지 않습니다. 정상 앱 실행은 보호된 `Live`이며 자동으로 비송출 모드로 전환되지 않습니다. CLI 절차에서는 모든 안전 게이트를 통과한 격리 `Test`의 `--test-connect`에서 처음 네트워크 기록을 기대하며, 별도로 승인된 UI Test 실행도 기록을 만들 수 있습니다. 사용할 포트는 예제 숫자가 아니라 해당 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port` 실값이어야 합니다. Tornado2의 `PGM` 창은 본 프로그램의 KTAP 명령을 렌더링하는 출력 창입니다. 현재 PGM과의 네트워크 왕복만 진단할 때는 일반 Test/Live 엔진을 완화하지 않고 `--pgm-connect-diagnostic`을 사용합니다. 이 명령은 Registry64에서 계산한 x64 K3D 네이티브 DLL과 Interop이 운영자 승인 SHA-256 핀 `MBN_STOCK_K3D_NATIVE_SHA256`/`MBN_STOCK_K3D_INTEROP_SHA256`과 각각 일치할 때만 동적으로 사용하며 `KTAPConnect → Disconnect`만 한 번 수행합니다. Scene player, 장면 로드, PREPARE, PLAY, STOP 계열 API 표면은 포함하지 않으며 해시 승인 절차와 판정 방법은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md)에 있습니다. diff --git a/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md b/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md index 4889b92..4f1d392 100644 --- a/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md +++ b/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md @@ -10,8 +10,8 @@ PGM이 개발 목적이며 방송에 영향을 주지 않는 환경임을 확인 상시 허용 범위는 개발 앱의 실행·정상 종료, CONNECT/DISCONNECT, PREPARE, TAKE IN, NEXT, Page NEXT, TAKE OUT, Network Monitoring/PGM 확인, 그리고 해당 검증에 필요한 -개발 DB 읽기·쓰기다. Debug의 Git 제외 로컬 development-live 설정과 고정된 K3D DLL -해시를 사용한다. Release와 일반 설치본의 기본 `DryRun`은 그대로 유지한다. +개발 DB 읽기·쓰기다. Debug와 Release 모두 Git 제외 로컬 development-live 설정과 고정된 +K3D DLL 해시를 사용해 `Live`로만 기동한다. 검증 실패 시 `DryRun`으로 전환하지 않는다. ## 기술적 중단 조건 diff --git a/docs/DEVELOPMENT_LIVE_HANDOFF.md b/docs/DEVELOPMENT_LIVE_HANDOFF.md index 0a7a51c..02ab674 100644 --- a/docs/DEVELOPMENT_LIVE_HANDOFF.md +++ b/docs/DEVELOPMENT_LIVE_HANDOFF.md @@ -8,9 +8,8 @@ 확인할 수 없으면 시작하지 말고 [운영·비지정 환경 승인 게이트](PLAYOUT_OPERATIONS.md#운영비지정-환경-승인-게이트)를 적용한다. -이 절차가 여는 Live 경로는 `Debug|x64`의 -`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)` 한 프로세스에만 -유효하다. `Release`와 일반 설치본의 기본 모드는 계속 `DryRun`이다. Release를 Live로 바꾸거나 +이 절차가 여는 Live 경로는 지정 개발 PC의 `Debug|x64`와 `Release|x64` +`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`에 유효하다. 사용자·시스템 환경 변수에 Live 승인을 영구 저장하지 않는다. ## Git 밖에서 확인하거나 준비할 항목 @@ -57,8 +56,8 @@ PowerShell 명령은 필요 없다. 1. 저장소를 clone하고 `MBN_STOCK_WEBVIEW.sln`을 Visual Studio 2026에서 연다. 2. `Debug|x64`, `Legacy Parity App (VS F5)`, `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택한다. -3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB 미연결과 DryRun을 - 강제하고 Tornado2/PGM에 연결하지 않는다. +3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB와 송출 엔진을 + 만들지 않고 Tornado2/PGM에 연결하지 않는다. 4. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다. 5. `Res 폴더 선택`에서 기존 코더가 사용하는 실제 `Res` 폴더를 선택한다. 6. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다. @@ -85,14 +84,14 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar ``` 출력에 외부 runtime 경로가 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드가 계속 -표시되면 Visual Studio에서 다시 빌드한다. `DryRun`은 경로 저장 실패가 아니라 별도의 -Development Live 승인이 아직 없다는 뜻일 수 있다. +표시되면 Visual Studio에서 다시 빌드한다. Live 기동이 차단되면 별도의 Development Live +승인이나 보호 설정이 아직 유효하지 않은 것이다. 같은 clone에서는 이후 `git pull` 뒤 Visual Studio 2026에서 `Debug|x64`, 시작 대상 `Legacy Parity App (VS F5)`, 실행 프로필 `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작할 수 있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 경로 저장을 다시 진행한다. DB overlay, K3D pin과 Development Live 승인이 없는 새 PC에서는 아래 수동 초기화 절차를 별도로 -완료하기 전까지 안전한 `DryRun`을 유지한다. K3D DLL 변경은 경로 저장을 반복해도 자동 +완료하기 전까지 전체 앱 기동을 차단한다. K3D DLL 변경은 경로 저장을 반복해도 자동 승인되지 않는다. 최초 실제 동작 확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다. @@ -341,7 +340,7 @@ powershell -NoProfile -ExecutionPolicy Bypass ` | 항목 | 인수 값 | |---|---| -| `mode` | 항상 `DryRun`. Development Live 프로세스만 일시적으로 Live로 덮어쓴다. | +| `mode` | 항상 `Live`. Debug와 Release가 같은 보호된 프로필을 사용한다. | | `host` | 로컬 PGM이면 숫자형 loopback `127.0.0.1`. `localhost` 또는 원격 주소를 추정하지 않는다. | | `port` | 지정 개발 환경의 고정 Network Server TCP port `30001` | | `tcpMode` / `clientPort` | `1` / `0` | @@ -369,7 +368,7 @@ Development Live로 승인된 프로세스는 이 26개 속성을 strict JSON으 scene root는 실행 파일 옆 검증된 `Cuts`로 마지막에 다시 고정한다. DB도 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini`를 우선하고, 없을 때만 보호된 `Config\database.local.json`을 사용하며 DB 환경 변수는 무시한다. 이 strict 경로가 성립하지 -않으면 Live로 진행하지 않고 안전한 DryRun으로 시작한다. +않으면 Live 엔진 기동을 차단하고 명확한 오류를 표시한다. ### `playout.development-live.local.json` @@ -427,12 +426,12 @@ timeout, `OutcomeUnknown`, 대상 PID·시작 시각·listener 변경 또는 결 ## 8. Development Live 최소 인수 시퀀스 1. 기존 앱 인스턴스를 정상 종료한다. -2. Visual Studio에서 구성 `Debug`, 플랫폼 `x64`, 시작 대상 +2. Visual Studio에서 구성 `Debug` 또는 `Release`, 플랫폼 `x64`, 시작 대상 `Legacy Parity App (VS F5)`를 선택한다. 3. 실행 프로필을 `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`로 선택하고 F5로 시작한다. -4. 앱이 소스 전용 또는 DryRun으로 시작했다면 실제 명령을 시도하지 않는다. runtime 초기화와 - 로컬 binding, 시작 인자와 두 로컬 파일을 고친 뒤 앱을 새로 시작한다. +4. 앱이 소스 전용이거나 Live 기동 차단 오류를 표시하면 실제 명령을 시도하지 않는다. + runtime 초기화와 로컬 binding, 시작 인자와 두 로컬 파일을 고친 뒤 앱을 새로 시작한다. 5. 최초 인수는 자산이 완전한 사전 합의 scene 하나만 사용한다. 기본 최소 회차는 active alias `5001`의 `CONNECT 1회 → PREPARE 1회 → 화면·Network Monitoring 확인 → TAKE IN 1회 → 화면 확인 → TAKE OUT 1회 → DISCONNECT 1회`다. @@ -445,13 +444,11 @@ timeout, `OutcomeUnknown`, 대상 PID·시작 시각·listener 변경 또는 결 allowlist 실패가 발생하면 다음 동작, 같은 동작 반복, 반대 동작이나 추측성 cleanup을 하지 않는다. PGM/Tornado2를 강제 종료하지 말고 상태를 보존한다. -## 9. DryRun 복귀와 인수 완료 조건 +## 9. Live 종료와 인수 완료 조건 Live 앱을 종료하면 부트스트랩이 만든 Live 환경은 프로세스와 함께 사라져야 한다. -`playout.local.json`의 `mode`는 계속 `DryRun`이어야 한다. 다음으로 -`MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)` 프로필을 실행해 실제 KTAP -통신 없이 UI가 시작되는지 확인한다. Release 검증도 이 DryRun 프로필로만 수행하며 -`--development-live`를 Release Live 수단으로 사용하지 않는다. +`playout.local.json`의 `mode`는 계속 `Live`여야 한다. 다음 실행도 같은 보호된 Live +부트스트랩을 사용하며, 별도의 비송출 프로필로 전환하지 않는다. 다음 항목이 모두 맞아야 인수를 완료한다. @@ -461,7 +458,7 @@ Live 앱을 종료하면 부트스트랩이 만든 Live 환경은 프로세스 - 개발 DB read-only smoke 성공 - 지속 K3D pin과 두 `%LOCALAPPDATA%` 송출 설정 파일의 내용·ACL 검증 - connect-only 진단과 최소 5001 회차가 retry 0, `OutcomeUnknown=false`로 종료 -- 최종 TAKE OUT/IDLE, DISCONNECT와 Explicit DryRun 재시작 확인 +- 최종 TAKE OUT/IDLE, DISCONNECT와 정상 Live 재시작 확인 - 누락 영상 14개 관련 action이 제한 상태임을 인수자에게 고지 실제 endpoint, 자격증명, 해시, 자산 경로, 화면 캡처와 Network Monitoring 증거는 승인된 diff --git a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md index 4bbf3d9..da1922f 100644 --- a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md +++ b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md @@ -11,16 +11,16 @@ 이 문서는 개발 PC의 Visual Studio 2026에서 `MBN_STOCK_WEBVIEW.LegacyParityApp`을 명확한 시작 대상으로 실행하고, 개발 Tornado2에 연결하는 방법을 설명한다. -저장소의 송출 기본값과 Release 동작은 계속 `DryRun`이다. 개발 Live는 다음 세 조건이 -동시에 맞는 **Debug 프로세스 한 회차**에서만 기존 Live 게이트를 설정한다. +지정 개발 PC의 Debug와 Release 전체 앱은 모두 보호된 `Live`로만 기동한다. 개발 Live는 +다음 조건이 동시에 맞을 때 기존 Live 게이트를 설정한다. -1. 시작 인자가 정확히 `--development-live` 하나이다. -2. 컴파일된 앱이 Debug 빌드이다. +1. 정상 무인자 실행이거나 시작 인자가 정확히 `--development-live` 하나이다. +2. 소스 전용 설정 빌드가 아닌 전체 runtime 빌드이다. 3. 아래 Git 제외 로컬 승인 파일이 존재하며 엄격한 스키마 검사를 통과한다. 조건이 하나라도 다르면 환경 변수를 설정하지 않는다. 로컬 파일이 없거나 읽기 권한이 -없고, JSON 또는 해시 형식이 잘못된 경우에도 시작은 fail-closed로 진행되어 기존 -`playout.local.json`의 안전 기본인 `DryRun`을 사용한다. +없고, JSON 또는 해시 형식이 잘못된 경우에는 전체 앱의 Live 엔진 기동을 차단하고 +명확한 오류를 표시한다. `DryRun`으로 폴백하지 않는다. 새 clone처럼 아직 검증된 runtime 연결이 없는 경우의 첫 F5는 예외적으로 소스 전용 설정 앱을 연다. 이 프로세스는 `--development-live`를 적용하지 않고 DB와 송출을 모두 차단한다. @@ -29,7 +29,7 @@ `dotnet build`, DB 복사, K3D 검사와 Live 승인 발급도 실행하지 않으므로 즉시 완료되어야 한다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 전체 앱 빌드가 시작된다. -기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서도 안전한 `DryRun`을 유지한다. +기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서 전체 앱 기동이 차단된다. DB overlay, K3D pin과 Development Live 승인이 필요하면 아래 수동 초기화 절차를 별도로 완료한다. 자세한 절차는 [개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를 @@ -50,24 +50,22 @@ src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj ``` `Debug|x64`와 `Release|x64`의 MSIX Deploy 대상도 루트 프로토타입이 아니라 이 프로젝트이다. Visual -Studio의 시작 드롭다운에는 다음 두 프로필이 표시된다. +Studio의 시작 드롭다운에는 다음 프로필 하나만 표시된다. | 순서 | 프로필 | 동작 | |---:|---|---| | 1 | `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)` | `--development-live`를 전달한다. 유효한 로컬 승인 파일이 있을 때만 Live를 구성한다. | -| 2 | `MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)` | 개발 Live 인자를 전달하지 않는다. | -개발 PC에서 Live를 사용할 때는 공유 실행 프로필 `Legacy Parity App (VS F5)`, 구성 -`Debug`, 플랫폼 `x64`, 첫 번째 Development Live 패키지 프로필을 선택하고 F5를 누른다. -DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴스이므로 프로필을 바꿀 때 -기존 인스턴스를 정상 종료한 뒤 다시 실행해야 한다. +개발 PC에서는 공유 실행 프로필 `Legacy Parity App (VS F5)`, 구성 `Debug` 또는 `Release`, +플랫폼 `x64`, Development Live 패키지 프로필을 선택하고 F5를 누른다. 앱은 단일 +인스턴스이므로 구성을 바꿀 때 기존 인스턴스를 정상 종료한 뒤 다시 실행해야 한다. 패키지된 WinUI 3 full-trust 실행에서는 Visual Studio가 프로세스 명령줄에 인자를 전달해도 `LaunchActivatedEventArgs.Arguments`가 빈 문자열일 수 있다. 부트스트랩은 activation 인자가 -비어 있을 때에만 `Environment.GetCommandLineArgs()`가 정확히 `exe + 인자 1개`인 경우 그 -하나를 사용한다. 인자가 없거나 둘 이상이면 합치거나 일부를 무시하지 않고 Live 요청을 -거부한다. Release 빌드는 이 fallback으로 정확한 인자를 얻더라도 Debug-only gate에서 -승인 파일을 읽지 않고 기존 `DryRun` 설정을 유지한다. +비어 있을 때에만 `Environment.GetCommandLineArgs()`를 확인한다. 인자가 없으면 정상 Live +실행으로 처리하고, 정확히 `exe + --development-live`이면 호환 Live 실행으로 처리한다. +추가 인자가 있으면 합치거나 일부를 무시하지 않고 기동을 거부한다. Release도 Debug와 +동일한 보호된 승인 파일과 검증을 사용한다. ## 로컬 승인 파일 @@ -137,8 +135,8 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴 %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\playout.local.json ``` -이 파일의 `mode`는 `DryRun`으로 유지한다. Development Live 부트스트랩이 성공한 해당 -Debug 프로세스에서만 `MBN_STOCK_PLAYOUT_MODE=Live`를 process scope로 덮어쓴다. +이 파일의 `mode`는 `Live`로 유지한다. Development Live 부트스트랩은 Debug와 Release +프로세스에서 검증된 Live authorization과 K3D 해시를 process scope로 적용한다. 기존 이중 게이트를 유지하기 위해 로컬 설정에는 `trustedLiveOutputEnabled: true`가 명시되어야 한다. @@ -170,6 +168,5 @@ Debug 프로세스에서만 `MBN_STOCK_PLAYOUT_MODE=Live`를 process scope로 `EnvironmentLiveAuthorization`은 `MainWindow`와 `IPlayoutEngine`을 만들 때 값을 한 번만 캡처하므로, 부트스트랩은 반드시 `new MainWindow()`보다 먼저 실행된다. -Release 빌드에서는 `--development-live`가 전달되어도 로컬 승인 파일을 읽지 않고 환경을 -변경하지 않는다. 이 개발 편의 경로는 기존 운영 Live 승인 경로나 안전 게이트를 대체하지 -않는다. +Release 빌드도 Debug와 같은 로컬 승인 파일을 읽고 같은 안전 게이트를 적용한다. 이는 +운영/방송 환경의 별도 승인 절차를 대체하지 않는다. diff --git a/docs/PLAYOUT.md b/docs/PLAYOUT.md index d7a574f..2514843 100644 --- a/docs/PLAYOUT.md +++ b/docs/PLAYOUT.md @@ -9,11 +9,11 @@ ## 안전 기준 -기본 모드는 `DryRun`입니다. `DryRun`은 COM 객체를 만들거나 Tornado 출력에 명령을 보내지 않고 WebView 메시지, 큐, 상태 및 오류 표시 흐름만 검증합니다. 현재 방송 PROGRAM(PGM)에 연결된 Tornado 프로세스는 항상 **안전하지 않은 대상**으로 취급합니다. 프로세스 이름이나 실행 여부만으로 테스트 대상이라고 판단하지 않습니다. +지정 개발 PC의 기본 모드는 보호된 `Live`입니다. 승인 파일·K3D 해시·scene/path allowlist 같은 불변식 검증에 실패하면 Live 엔진 기동을 차단하며 비송출 모드로 폴백하지 않습니다. K3D/PGM/DB가 일시적으로 준비되지 않은 경우에는 Live 엔진을 `Disconnected` 또는 `Faulted`로 유지합니다. 현재 방송 PROGRAM(PGM)에 연결된 Tornado 프로세스는 항상 **안전하지 않은 대상**으로 취급합니다. 프로세스 이름이나 실행 여부만으로 테스트 대상이라고 판단하지 않습니다. 운영/방송 대상의 작업과 자동 검증은 실제 PGM 출력 또는 라이브 `TAKE IN`을 자동 허가하지 않습니다. `Test` 검증은 PGM과 분리된 전용 테스트 인스턴스, 테스트 출력 채널 및 허용 목록에 든 테스트 씬을 모두 확인한 뒤에만 수행합니다. 단, 위에 링크한 현재 개발 PC의 상시 권한 범위에서는 별도의 회차별 허가를 다시 요청하지 않습니다. -이 문서의 adapter·builder·DryRun 완료 표현은 화면/계약 구현 범위다. 운영 DB-W, 외부 asset readiness, 기존 데이터 복원과 장면별 실제 PGM은 별도 증거가 있어야 한다. 현재 미적용·적용 중·외부자산 필요 항목은 [`LEGACY_FEATURE_AUDIT.md`](LEGACY_FEATURE_AUDIT.md)를 따른다. +이 문서의 과거 adapter·builder·DryRun 완료 표현은 당시 화면/계약 구현 기록이다. 운영 DB-W, 외부 asset readiness, 기존 데이터 복원과 장면별 실제 PGM은 별도 증거가 있어야 한다. 현재 미적용·적용 중·외부자산 필요 항목은 [`LEGACY_FEATURE_AUDIT.md`](LEGACY_FEATURE_AUDIT.md)를 따른다. ## 확인된 x64 COM 등록 @@ -93,9 +93,9 @@ SDK가 기본 위치에 없다면 x64 SDK의 `TlbImp.exe` 절대 경로를 `-Tlb %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json ``` -[playout.example.json](../Config/playout.example.json)을 `playout.local.json`의 구조 참고용으로 사용합니다. 예시는 `DryRun`, 출력 채널 미지정, 빈 씬 허용 목록, 라이브 신뢰 플래그 해제 상태이므로 실제 출력에 사용할 수 없습니다. `runtime-folders.local.json`은 좌측 `설정` 메뉴에서 네이티브 폴더 선택 창으로 지정한 디자인(`Cuts`), 설정(`Res`), 운영 배경 폴더와 왼쪽 메뉴 시작 상태를 앱이 자동 저장하는 파일이므로 직접 편집하지 않습니다. 폴더 변경은 앱을 다시 시작한 뒤 자산·카탈로그에 적용되며, 메뉴 펼침 상태는 즉시 적용됩니다. +[playout.example.json](../Config/playout.example.json)을 `playout.local.json`의 구조 참고용으로 사용합니다. 예시는 `Live` 형식이지만 출력 채널 미지정, 빈 씬 허용 목록, 라이브 신뢰 플래그 해제 상태이므로 실제 출력에 사용할 수 없습니다. 실제 개발 PC 설정은 보호된 초기화 스크립트로만 생성합니다. `runtime-folders.local.json`은 좌측 `설정` 메뉴에서 네이티브 폴더 선택 창으로 지정한 디자인(`Cuts`), 설정(`Res`), 운영 배경 폴더와 왼쪽 메뉴 시작 상태를 앱이 자동 저장하는 파일이므로 직접 편집하지 않습니다. 폴더 변경은 앱을 다시 시작한 뒤 자산·카탈로그에 적용되며, 메뉴 펼침 상태는 즉시 적용됩니다. -송출 장면 루트는 환경 변수, 사용자 설정 메뉴, `playout.local.json`, 실행 파일 옆 기본 `Cuts` 순으로 우선합니다. 운영 배경 루트도 환경 변수, 사용자 설정 메뉴, `playout.local.json` 순으로 우선하며 모두 비어 있으면 최종 장면 루트의 sibling `배경` 폴더를 사용합니다. 사용자 설정 메뉴가 덮어쓰는 송출 값은 이 두 자산 루트뿐입니다. Release/default `DryRun`, KTAP 호스트·포트·채널, Test/Live 게이트, allowlist와 벤더 해시는 이 화면에서 편집할 수 없고 기존 검사를 그대로 통과해야 합니다. 별도 씬 루트나 보호 설정을 명시할 때는 테스트 장비의 호스트, 채널, 창 제목 패턴과 허용할 씬 이름을 로컬 파일에만 기록하고 Git, 로그 또는 지원 첨부파일에 넣지 않습니다. +송출 장면 루트는 환경 변수, 사용자 설정 메뉴, `playout.local.json`, 실행 파일 옆 기본 `Cuts` 순으로 우선합니다. 운영 배경 루트도 환경 변수, 사용자 설정 메뉴, `playout.local.json` 순으로 우선하며 모두 비어 있으면 최종 장면 루트의 sibling `배경` 폴더를 사용합니다. 사용자 설정 메뉴가 덮어쓰는 송출 값은 이 두 자산 루트뿐입니다. 보호된 `Live` 모드, KTAP 호스트·포트·채널, Test/Live 게이트, allowlist와 벤더 해시는 이 화면에서 편집할 수 없고 기존 검사를 그대로 통과해야 합니다. 별도 씬 루트나 보호 설정을 명시할 때는 테스트 장비의 호스트, 채널, 창 제목 패턴과 허용할 씬 이름을 로컬 파일에만 기록하고 Git, 로그 또는 지원 첨부파일에 넣지 않습니다. | 속성 | 의미 | |---|---| @@ -151,9 +151,9 @@ MBN_STOCK_PLAYOUT_RECONNECT_ENABLED ### KTAP 포트와 Network Monitoring 판정 -K3DAsyncEngine 매뉴얼의 `KTAPConnect(bTCP, HostAddress, nHostPort, nClientPort, handler)`에서 Test/Live는 `tcpMode: 1`(TCP)만 허용하므로 로컬 JSON의 `port`는 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port`와 정확히 같아야 합니다. `clientPort`는 UDP일 때만 의미가 있고 `TAP TCP Port`/`TAP UDP Port`는 이 연결의 host port가 아닙니다. 매뉴얼 18쪽의 설명과 26쪽 및 192쪽 예시에 표시된 포트 숫자가 서로 다르므로 `30001`/`30002`를 추정하거나 원본·예제 값을 복사하지 않습니다. 해당 회차 Test 인스턴스 화면의 설정값이 유일한 기준입니다. `playout.example.json`의 `30001`도 DryRun 구조 예시일 뿐 새 Test endpoint의 검증값이 아닙니다. +K3DAsyncEngine 매뉴얼의 `KTAPConnect(bTCP, HostAddress, nHostPort, nClientPort, handler)`에서 Test/Live는 `tcpMode: 1`(TCP)만 허용하므로 로컬 JSON의 `port`는 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port`와 정확히 같아야 합니다. `clientPort`는 UDP일 때만 의미가 있고 `TAP TCP Port`/`TAP UDP Port`는 이 연결의 host port가 아닙니다. 매뉴얼 18쪽의 설명과 26쪽 및 192쪽 예시에 표시된 포트 숫자가 서로 다르므로 `30001`/`30002`를 추정하거나 원본·예제 값을 복사하지 않습니다. 해당 회차 Test 인스턴스 화면의 설정값이 유일한 기준입니다. `playout.example.json`의 `30001`도 구조 예시일 뿐 새 Test endpoint의 검증값이 아닙니다. -Tornado2의 `View > Network Monitoring Window`에서 `[R]`은 서버가 클라이언트 요청을 받은 기록, `[S]`는 서버가 응답을 보낸 기록이며 `TCPSession`은 TCP 세션 수입니다(매뉴얼 26~27쪽). 프로세스 감지, COM 등록 probe 또는 COM 객체 생성만으로는 이 기록이 생기지 않습니다. 기본 앱과 `--dry-run`, `--probe`, `--test-plan`은 KTAP를 호출하지 않으므로 빈 모니터가 정상입니다. +Tornado2의 `View > Network Monitoring Window`에서 `[R]`은 서버가 클라이언트 요청을 받은 기록, `[S]`는 서버가 응답을 보낸 기록이며 `TCPSession`은 TCP 세션 수입니다(매뉴얼 26~27쪽). 프로세스 감지, COM 등록 probe 또는 COM 객체 생성만으로는 이 기록이 생기지 않습니다. `--probe`와 `--test-plan`은 KTAP를 호출하지 않으므로 빈 모니터가 정상입니다. 정상 앱 실행은 보호된 `Live`이므로 CONNECT 시 네트워크 기록을 기대합니다. 상태의 `accepted-unconfirmed`는 `KTAPConnect`가 SDK 성공값 `1`을 반환했다는 뜻일 뿐입니다. 매뉴얼 41쪽의 `OnHello` 콜백이나 실제 `[R]`/`[S]`를 자동 확인했다는 뜻이 아닙니다. connect-only `--pgm-connect-diagnostic`은 렌더 API 표면을 제거한 별도 binding이라 callback을 관찰하지 않고 `ktapHelloObserved`를 `null`로 보고합니다. 일반 `IPlayoutEngine` 경로는 검증된 282-method `DynamicK3dEventHandler`로 `OnHello`와 lifecycle callback을 수신하지만, callback 미수신 상태를 성공으로 추정하지 않으며 Network Monitoring은 계속 사람이 대조합니다. `lastKtapConnectState`는 현재 연결 상태가 아니라 가장 최근 KTAP dispatch 시도의 증거이며, 화면은 `Connected`/`Faulted` 같은 현재 상태와 분리해 표시합니다. 따라서 격리 `--test-connect`가 성공했는데도 같은 시각의 `[R]`/`[S]`가 전혀 없다면 `--test-sequence`로 진행하지 말고 mode/config 파일, 실제 Network Server TCP Port와 안전 게이트 거부 여부를 먼저 확인합니다. @@ -257,7 +257,7 @@ Test에서 로컬 프로세스/창 제목 검사를 원격 KTAP endpoint의 신 ## 안전한 스모크 절차 -먼저 `mode`가 `DryRun`이고 라이브 환경 변수가 없는지 확인합니다. +먼저 보호된 Live 설정·승인 파일과 K3D 등록을 읽기 전용으로 확인합니다. ```powershell Remove-Item Env:MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT -ErrorAction SilentlyContinue @@ -265,10 +265,8 @@ powershell -NoProfile -ExecutionPolicy Bypass ` -File .\scripts\Inspect-K3DRegistration.ps1 dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke ` -c Debug -p:Platform=x64 -- --probe -dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke ` - -c Debug -p:Platform=x64 -- --dry-run -dotnet test .\MBN_STOCK_WEBVIEW.sln -c Debug -p:Platform=x64 -dotnet test .\MBN_STOCK_WEBVIEW.sln -c Release -p:Platform=x64 +dotnet build .\MBN_STOCK_WEBVIEW.sln -c Debug -p:Platform=x64 +dotnet build .\MBN_STOCK_WEBVIEW.sln -c Release -p:Platform=x64 powershell -NoProfile -ExecutionPolicy Bypass ` -File .\scripts\Test-WebPlayout.ps1 ``` @@ -392,7 +390,7 @@ Network Monitoring 최종 증분은 HELLO 1/1, 5001/5074 LOAD 각각 1/1, 5001 P Round H cleanup은 승인된 TAKE OUT 1회와 두 scene unload, disconnect, 앱·진단 listener 종료, 회차 전용 Live 설정과 승인 환경 제거, PGM/Network Monitoring 창 상태 복원으로 끝났습니다. 정상 회차라 추정 rollback은 실행하지 않았습니다. 장애 rollback은 기본 `DryRun`/`Disabled`로 복귀하거나 조직 절차로 직전 승인 패키지를 복원하는 범위이며, vendor DLL·COM 등록·라이선스·실제 자산은 수정하거나 저장소에 넣지 않습니다. 실제 Live PGM 검증 범위는 허용된 5001/5074뿐이고, 35개 scene 전체 완료 근거는 자동 테스트·55-query 실데이터 smoke·매트릭스입니다. -패키지 스모크에서는 벤더 x64 COM이 장비에 정식 등록되어 있어야 합니다. MSIX에 벤더 DLL을 복사해 활성화 오류를 우회하지 않습니다. 패키지 컨텍스트에서 COM 활성화가 막히면 `DryRun` 또는 `Disabled`를 유지하고 HRESULT와 등록 검사 결과만 보고합니다. +패키지 스모크에서는 벤더 x64 COM이 장비에 정식 등록되어 있어야 합니다. MSIX에 벤더 DLL을 복사해 활성화 오류를 우회하지 않습니다. 패키지 컨텍스트에서 COM 활성화가 막히면 Live 엔진을 `Faulted`/`Disconnected`로 유지하고 HRESULT와 등록 검사 결과만 보고합니다. ## 장애 및 롤백 diff --git a/docs/PLAYOUT_OPERATIONS.md b/docs/PLAYOUT_OPERATIONS.md index 536464d..4796d12 100644 --- a/docs/PLAYOUT_OPERATIONS.md +++ b/docs/PLAYOUT_OPERATIONS.md @@ -14,7 +14,7 @@ | 항목 | 상태 | |---|---| -| 기본 모드 | `DryRun`; COM 객체와 `KTAPConnect`를 만들지 않음 | +| 지정 개발 PC 기본 모드 | 보호된 `Live`; 승인·설정 불변식 실패 시 기동 차단, 자동 `DryRun` 폴백 없음 | | 자동 테스트 | 최신 Release x64 .NET solution 3,182/3,182, LegacyParityWeb 150/150, WebPlayout JavaScript 424/424, 원본 scene 기준선 35/35와 staged PGM 정적 계약 통과; 실패·skip 0건 | | 개발 MSIX UI/DB | 최신 등록 AppX에서 실제 Windows 입력 1,838건으로 UC1~UC7, GraphE, FSell, VIList, ThemeA, EList, PList 모달, 설정·비교 가져오기와 개발 DB 저장→fresh readback→delete/absence를 검증하고 cleanup 완료 | | 고객 배포 MSIX | 현재 범위에서 보류. 고객용 version·Publisher·서명 인증서·업데이트와 새 PC 로컬 설정 배포는 사용자가 재개할 때 결정 | @@ -174,8 +174,8 @@ SHA-256 `7A1141C5F276729E340227CEEAA48B0E6018632BBA51F43AA94636084AA3C301`이며 ## 절대 안전 규칙 -1. 앱의 기본값은 항상 `DryRun`으로 유지한다. -2. `DryRun`, `--probe`, `--dry-run`, `--test-plan`은 `KTAPConnect`를 호출하지 않는다. 이때 Tornado2 Network Monitoring에 기록이 없는 것이 정상이다. +1. 지정 개발 PC의 앱은 보호된 `Live`로만 기동하고 `DryRun`으로 전환하지 않는다. +2. `--probe`와 `--test-plan`은 `KTAPConnect`를 호출하지 않는다. 이때 Tornado2 Network Monitoring에 기록이 없는 것이 정상이다. 3. 실제 출력은 허용된 테스트 scene, 지정된 PGM, 지정된 회차 범위에서만 수행한다. 4. 운영·방송 또는 비지정 환경은 아래 Gate A/B의 회차별 승인을 적용한다. 현재 지정 개발 PC는 [개발 환경 상시 권한](DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md)의 범위·대상 확인·기술적 중단 조건을 적용하며 별도 대화 승인을 반복하지 않는다. 5. 회차 범위에 없는 NEXT, Page NEXT, TAKE OUT 이외 명령 또는 다른 scene으로 범위를 넓히지 않는다. @@ -301,9 +301,9 @@ PREPARE 결과와 Network Monitoring 상태를 운영자가 확인한 후, TAKE 5. K3D Registry64, AMD64 PE, TypeLib/Interop metadata, 두 SHA-256 pin, license를 확인한다. DLL을 repo 또는 앱 폴더로 복사해 우회하지 않는다. 6. 정확히 하나의 승인 대상 Tornado2 프로세스, PGM 창, Network Server TCP port와 LISTEN 소유권을 확인한다. 매뉴얼 예시 port를 추정해 사용하지 않는다. 7. `s5025`를 선택했다면 trusted 외부 디렉터리와 파일 preflight가 성공해야 한다. `s5006`은 `Video\큐브배경.vrv`, `s6001` 해외지수는 선택 국가에 해당하는 `Video\20201008_<국가>.vrv`가 승인 asset root 안에 있어야 한다. coverage 결과의 `MissingAssets`, `RootEscapes`, `ReparseAssets` 중 하나라도 0이 아니면 해당 장면으로 진행하지 않는다. -8. 실제 COM을 사용하는 Test/Live playlist의 모든 cut이 로컬 폐쇄형 allowlist 안에 있고 selector가 closed enum/lookup 규칙을 통과하는지 DryRun에서 확인한다. allowlist가 비어 있으면 실제 모드 초기화 자체를 거부해야 한다. -9. 로컬 playout 설정의 `legacySceneFadeDuration` 기본값 6과 `legacySceneBackgroundKind`를 확인한다. 공통 background를 쓸 때 `legacySceneBackgroundAssetPath`는 `sceneDirectory` 아래의 승인된 상대 경로여야 한다. `PlayoutSceneCompositionFactory`의 DryRun preflight가 파일 존재, 허용 확장자, root 탈출, 절대 경로와 reparse point를 모두 거부하는지 확인한다. Web에는 이 asset 경로를 보내지 않는다. -10. Web DryRun에서 45개 active alias, row별 `enabled`, PREPARE 뒤 snapshot freeze, current entry/builder/page size/current rows/last-page/preview와 refresh 상태를 확인한다. pending command와 `OutcomeUnknown`/timeout quarantine에서도 playlist 편집이 잠겨야 한다. preview에 image/texture/video 경로가 나타나면 실제 회차를 중단한다. +8. 실제 COM을 사용하는 Test/Live playlist의 모든 cut이 로컬 폐쇄형 allowlist 안에 있고 selector가 closed enum/lookup 규칙을 통과하는지 정적 계약 테스트에서 확인한다. allowlist가 비어 있으면 실제 모드 초기화 자체를 거부해야 한다. +9. 로컬 playout 설정의 `legacySceneFadeDuration` 기본값 6과 `legacySceneBackgroundKind`를 확인한다. 공통 background를 쓸 때 `legacySceneBackgroundAssetPath`는 `sceneDirectory` 아래의 승인된 상대 경로여야 한다. `PlayoutSceneCompositionFactory` preflight가 파일 존재, 허용 확장자, root 탈출, 절대 경로와 reparse point를 모두 거부하는지 확인한다. Web에는 이 asset 경로를 보내지 않는다. +10. Web 계약 테스트에서 45개 active alias, row별 `enabled`, PREPARE 뒤 snapshot freeze, current entry/builder/page size/current rows/last-page/preview와 refresh 상태를 확인한다. pending command와 `OutcomeUnknown`/timeout quarantine에서도 playlist 편집이 잠겨야 한다. preview에 image/texture/video 경로가 나타나면 실제 회차를 중단한다. 하나라도 실패하면 회차를 시작하지 않는다. 설정을 수정한 뒤 처음부터 새 preflight 결과를 만든다. @@ -396,7 +396,8 @@ vendor monitor의 실제 명령 표기는 버전에 따라 다를 수 있으므 | 상황 | 즉시 조치 | 재시도 조건 | |---|---|---| -| DB/selector/asset preflight 실패, COM 호출 전 명시적 `Rejected` | 회차 중단, DryRun으로 복귀, 원인 기록 | offline 수정과 전체 preflight 후 새 회차 승인 | +| 승인/설정/allowlist 불변식 실패, COM 호출 전 명시적 `Rejected` | Live 엔진 기동 차단, 원인 기록 | offline 수정과 전체 preflight 후 새 실행 | +| DB/K3D/PGM 일시 장애 | Live 엔진을 `Disconnected`/`Faulted`로 유지하고 명령 전송 중단 | 원인 수정 후 known state에서 명시적 CONNECT | | Connect timeout 또는 `OnHello`/monitor 불일치 | 결과를 불명확으로 격리, Connect 반복 금지 | 운영자가 세션과 process/port 상태를 확인한 뒤 새 회차 승인 | | PREPARE/TAKE IN/NEXT/refresh/TAKE OUT timeout | `OutcomeUnknown`으로 취급, 같은 명령과 반대 명령 자동 실행 금지 | PGM/monitor/콜백을 사람이 확인하고 출력 안전을 복구한 뒤 새 회차 승인 | | `WEB_TIMEOUT` | strict timeout-quarantine message를 native에 전달. MainWindow가 process-lifetime latch를 먼저 세우고 vendor session을 quarantine; 늦은 응답, UI 재시도와 WebView reload로 해제 금지 | native status와 PGM을 사람이 대조하고 process를 새로 시작한 뒤 새 승인 | diff --git a/scripts/Initialize-DevelopmentLiveConfig.ps1 b/scripts/Initialize-DevelopmentLiveConfig.ps1 index 1eb3e9a..f8ffd2b 100644 --- a/scripts/Initialize-DevelopmentLiveConfig.ps1 +++ b/scripts/Initialize-DevelopmentLiveConfig.ps1 @@ -180,7 +180,7 @@ Assert-WritableTarget $playoutPath Assert-WritableTarget $authorizationPath $playout = [ordered]@{ - mode = 'DryRun' + mode = 'Live' host = $PlayoutHost port = $PlayoutPort tcpMode = 1 @@ -219,20 +219,19 @@ $authorization = [ordered]@{ $playoutJson = $playout | ConvertTo-Json -Depth 6 $authorizationJson = $authorization | ConvertTo-Json -Depth 3 -# In Force mode, invalidate the old authorization before changing the DryRun -# base. Any later failure therefore leaves no valid authorization at the exact -# path consumed by the Debug-only bootstrap. +# In Force mode, invalidate the old authorization before changing the protected +# Live profile. Any later failure therefore leaves no valid authorization at the +# exact path consumed by the bootstrap. Invalidate-ExistingAuthorization -Path $authorizationPath -# Write the safe DryRun base first and the new one-launch authorization last. -# Merely creating these files does not connect: Debug, the exact -# --development-live argument, vendor hash verification and runtime gates remain -# mandatory in the application. +# Write the protected Live profile first and the launch authorization last. +# Merely creating these files does not connect: vendor hash verification, +# launch authorization, target validation, and runtime gates remain mandatory. Write-ProtectedJson $playoutPath $playoutJson Write-ProtectedJson $authorizationPath $authorizationJson Write-Host "Development Live base configuration created at: $playoutPath" Write-Host "Development Live launch authorization created at: $authorizationPath" Write-Warning ( - 'The base file remains DryRun. Use only Debug|x64 with the exact ' + - 'Development Live (Package) profile after confirming the local development PGM target.') + 'Debug and Release launches now use the protected Live profile. ' + + 'Confirm the local development PGM target before starting the application.') diff --git a/scripts/Initialize-ExistingDevelopmentPc.ps1 b/scripts/Initialize-ExistingDevelopmentPc.ps1 index 3f5b1dc..e0564da 100644 --- a/scripts/Initialize-ExistingDevelopmentPc.ps1 +++ b/scripts/Initialize-ExistingDevelopmentPc.ps1 @@ -1416,7 +1416,7 @@ function Get-DevelopmentLivePreflight { '8001', '8002', '8003', '8018', '8032', '8035', '8040', '8046', '8051', '8056', '8061', '8067', 'N5001') $playout = [ordered]@{ - mode = 'DryRun' + mode = 'Live' host = $HostName port = $Port tcpMode = 1 diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs index 5d41157..3163da6 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs @@ -14,6 +14,10 @@ public partial class App : Application internal static bool IsDevelopmentLiveLaunchRequested { get; private set; } + internal static string? DevelopmentLiveStartupFailureCode { get; private set; } + + internal static string? DevelopmentLiveStartupFailureMessage { get; private set; } + public App() { InitializeComponent(); @@ -43,40 +47,31 @@ public partial class App : Application _mainInstance = registeredInstance; _mainInstance.Activated += OnMainInstanceActivated; -#if DEBUG - const bool isDebugBuild = true; -#else - const bool isDebugBuild = false; -#endif - -#if SOURCE_ONLY_RUNTIME - const bool isSourceOnlyBuild = true; -#else - const bool isSourceOnlyBuild = false; -#endif - var launchArguments = isSourceOnlyBuild - ? null - : DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments( - args.Arguments, - Environment.GetCommandLineArgs()); - IsDevelopmentLiveLaunchRequested = string.Equals( - launchArguments, - DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, - StringComparison.Ordinal); - var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply( - launchArguments, - isDebugBuild); - IsDevelopmentLiveLaunch = developmentLive.IsApplied; - System.Diagnostics.Debug.WriteLine( - $"Development Live bootstrap: {developmentLive.Code}"); - // A fresh clone cannot become a full runtime inside the already compiled // source-only process. Show only the native two-folder setup window; the // reviewed initializer builds the verified runtime for the next F5. - var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild; - _window = showFirstRunSetup - ? new FirstRunSetupWindow() - : new MainWindow(); +#if SOURCE_ONLY_RUNTIME + IsDevelopmentLiveLaunchRequested = false; + IsDevelopmentLiveLaunch = false; + DevelopmentLiveStartupFailureCode = null; + DevelopmentLiveStartupFailureMessage = null; + _window = new FirstRunSetupWindow(); +#else + var launchArguments = DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments( + args.Arguments, + Environment.GetCommandLineArgs()); + var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(launchArguments); + IsDevelopmentLiveLaunchRequested = developmentLive.IsRequested; + IsDevelopmentLiveLaunch = developmentLive.IsApplied; + DevelopmentLiveStartupFailureCode = developmentLive.IsLaunchAllowed + ? null + : developmentLive.Code; + DevelopmentLiveStartupFailureMessage = developmentLive.FailureMessage; + System.Diagnostics.Debug.WriteLine( + $"Development Live bootstrap: {developmentLive.Code}"); + + _window = new MainWindow(); +#endif _window.Activate(); } diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj index df8ea79..81d7d43 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj @@ -205,7 +205,7 @@ Text="External legacy runtime paths are enabled. Cuts files remain in the selected folder and database credentials remain in the current user's LocalAppData overlay." /> + Text="Legacy runtime source root was not found: $(LegacyRuntimeSourceRoot). Run scripts\Initialize-ExistingDevelopmentPc.ps1 to create the verified local Required binding, or use /p:LegacyRuntimeAssetsMode=Auto for a source-only setup build." /> diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs index 2320458..524f7fe 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs @@ -32,21 +32,33 @@ public sealed partial class MainWindow { try { + if (_developmentLiveStartupBlocked) + { + _playoutInitializationError = + App.DevelopmentLiveStartupFailureMessage ?? + "Live playout startup validation failed."; + return; + } + var explicitLocalConfigurationExists = File.Exists( PlayoutOptionsLoader.DefaultPath); _playoutOptions = - _isDevelopmentLiveLaunch && - !_developmentLiveStartupBlocked + _isDevelopmentLiveLaunch ? PlayoutOptionsLoader.LoadDevelopmentLive( baseDirectory: AppContext.BaseDirectory, operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory) : PlayoutOptionsLoader.Load( operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory, - operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory, - forceSafeDryRun: IsSourceOnlyBuild || - _isDevelopmentLiveLaunchRequested || - _developmentLiveStartupBlocked); + operatorBackgroundDirectory: + _appliedOperatorSettings.BackgroundDirectory); + if (_playoutOptions.Mode != PlayoutMode.Live) + { + throw new PlayoutConfigurationException( + "The application requires a validated Live playout configuration. " + + "DryRun fallback is disabled."); + } + ResetRefreshState(); var startupComposition = LegacyParityStartupCompositionResolver.Resolve( _playoutOptions, @@ -75,6 +87,7 @@ public sealed partial class MainWindow } catch (Exception exception) { + _developmentLiveStartupBlocked = true; _playoutInitializationWarning = null; _playoutInitializationError = exception is PlayoutConfigurationException ? exception.Message diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs index 73f501b..a33d7ab 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs @@ -64,6 +64,9 @@ public sealed partial class MainWindow : Window { _isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch; _isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested; + _developmentLiveStartupBlocked = + _isDevelopmentLiveLaunchRequested && + !_isDevelopmentLiveLaunch; // Capture and remove Gate A bearer material before XAML can create WebView2. // Only the native process keeps the capability digest for this launch. _playoutLaunchAuthorization = @@ -119,9 +122,15 @@ public sealed partial class MainWindow : Window IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService; CorePagePlanProvider? fixedPagePlanProvider = null; string? initializationError = null; - var databaseInitializationSucceeded = false; try { + if (_developmentLiveStartupBlocked) + { + throw new InvalidOperationException( + App.DevelopmentLiveStartupFailureMessage ?? + "Live playout startup validation failed."); + } + if (IsSourceOnlyBuild) { throw new InvalidOperationException( @@ -198,14 +207,16 @@ public sealed partial class MainWindow : Window new LegacyStockMasterIdentityValidationService(_databaseRuntime.Executor); operatorCatalogSchemaValidationService = new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor); - databaseInitializationSucceeded = true; } catch { _databaseRuntime = null; fixedPagePlanProvider = null; - initializationError = IsSourceOnlyBuild - ? "소스 전용 DryRun에서는 데이터베이스 연결을 사용하지 않습니다." + initializationError = _developmentLiveStartupBlocked + ? App.DevelopmentLiveStartupFailureMessage ?? + "Live playout startup validation failed." + : IsSourceOnlyBuild + ? "소스 전용 설정 빌드에서는 데이터베이스 연결을 사용하지 않습니다." : "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요."; stockLookup = new UnavailableStockLookup(initializationError); industrySelectionService = new UnavailableIndustrySelectionService( @@ -241,14 +252,6 @@ public sealed partial class MainWindow : Window DataQueryExecutor.Reset(); } - if (_isDevelopmentLiveLaunch && !databaseInitializationSucceeded) - { - // Development Live is one fail-closed startup transaction. A - // missing or partially initialized protected DB runtime must not - // leave the independently armed playout gate eligible for use. - _developmentLiveStartupBlocked = true; - } - _industryWorkflow = new LegacyIndustrySelectionWorkflow( industrySelectionService, actionLayout: runtimeUiCatalogs.IndustryLayout); diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json index eefb321..ef70d60 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json @@ -3,10 +3,6 @@ "MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)": { "commandName": "MsixPackage", "commandLineArgs": "--development-live" - }, - "MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)": { - "commandName": "MsixPackage", - "commandLineArgs": "" } } } diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs index ef882f5..ddc52ad 100644 --- a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs +++ b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs @@ -1,6 +1,7 @@ using System.Security; using System.Text.Json; using MBN_STOCK_WEBVIEW.Playout.Diagnostics; +using MBN_STOCK_WEBVIEW.Playout.Safety; namespace MBN_STOCK_WEBVIEW.Playout.Configuration; @@ -8,6 +9,7 @@ internal enum DevelopmentLiveBootstrapStatus { Skipped, Applied, + GateABypassed, Rejected } @@ -16,6 +18,31 @@ internal readonly record struct DevelopmentLiveBootstrapResult( string Code) { public bool IsApplied => Status == DevelopmentLiveBootstrapStatus.Applied; + + public bool IsLaunchAllowed => + Status is DevelopmentLiveBootstrapStatus.Applied or + DevelopmentLiveBootstrapStatus.GateABypassed; + + public bool IsRequested => + Status is DevelopmentLiveBootstrapStatus.Applied or + DevelopmentLiveBootstrapStatus.Rejected; + + public string? FailureMessage => Status == DevelopmentLiveBootstrapStatus.Rejected + ? Code switch + { + "development-live-arguments-invalid" => + "Live startup was rejected because the launch arguments were not recognized.", + "development-live-gate-a-inspection-failed" => + "Gate A launch authorization could not be inspected.", + "development-live-authorization-unavailable" => + "Live startup authorization is unavailable.", + "development-live-authorization-invalid" => + "Live startup authorization is invalid.", + "development-live-environment-failed" => + "Live startup authorization could not be applied to this process.", + _ => "Live startup authorization failed." + } + : null; } internal interface IDevelopmentLiveBootstrapPlatform @@ -28,10 +55,10 @@ internal interface IDevelopmentLiveBootstrapPlatform } /// -/// Arms the existing process-scoped Live and vendor-binary gates for one explicit -/// Visual Studio Debug launch. The ordinary app startup path never calls this with -/// a true debug-build capability, and the authorization file is deliberately kept -/// outside the package and source tree. +/// Arms the existing process-scoped Live and vendor-binary gates for a full-runtime +/// application launch. Both ordinary no-argument launches and the retained exact +/// development-live argument use the same strict local authorization file, which +/// is deliberately kept outside the package and source tree. /// internal static class DevelopmentLiveLaunchBootstrap { @@ -46,6 +73,8 @@ internal static class DevelopmentLiveLaunchBootstrap private const string AuthorizationProperty = "authorization"; private const string NativeSha256Property = "nativeSha256"; private const string InteropSha256Property = "interopSha256"; + private const string InvalidLaunchArgumentsMarker = + "\0MBN_STOCK_WEBVIEW_INVALID_LAUNCH_ARGUMENTS"; private static readonly IReadOnlySet AllowedProperties = new HashSet(StringComparer.Ordinal) @@ -65,6 +94,14 @@ internal static class DevelopmentLiveLaunchBootstrap PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable ]; + private static readonly string[] GateAEnvironmentNames = + [ + PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable, + PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable, + PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable, + PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable + ]; + internal static string DefaultPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MBN_STOCK_WEBVIEW", @@ -86,11 +123,22 @@ internal static class DevelopmentLiveLaunchBootstrap return activationArguments; } - return processArguments.Count == 2 - ? processArguments[1] - : null; + return processArguments.Count switch + { + <= 1 => null, + 2 => processArguments[1], + _ => InvalidLaunchArgumentsMarker + }; } + internal static DevelopmentLiveBootstrapResult TryApply( + string? launchArguments) => + TryApply( + launchArguments, + isDebugBuild: true, + DefaultPath, + new SystemDevelopmentLiveBootstrapPlatform()); + internal static DevelopmentLiveBootstrapResult TryApply( string? launchArguments, bool isDebugBuild) => @@ -108,21 +156,32 @@ internal static class DevelopmentLiveLaunchBootstrap { ArgumentNullException.ThrowIfNull(platform); - if (!string.Equals( + // Retain the isDebugBuild parameter for callers compiled against the + // previous API. Release and Debug now intentionally share this exact gate. + _ = isDebugBuild; + + if (!string.IsNullOrEmpty(launchArguments) && + !string.Equals( launchArguments, ExactLaunchArgument, StringComparison.Ordinal)) { - return new DevelopmentLiveBootstrapResult( - DevelopmentLiveBootstrapStatus.Skipped, - "development-live-not-requested"); + return Rejected("development-live-arguments-invalid"); } - if (!isDebugBuild) + if (!TryHasGateAEnvironment(platform, out var hasGateAEnvironment)) + { + return Rejected("development-live-gate-a-inspection-failed"); + } + + // Gate A owns and consumes its process-scoped bearer values in + // PlayoutLaunchAuthorization. Do not read the ordinary authorization file + // and do not set, clear or roll back any environment value in this branch. + if (hasGateAEnvironment) { return new DevelopmentLiveBootstrapResult( - DevelopmentLiveBootstrapStatus.Skipped, - "development-live-debug-only"); + DevelopmentLiveBootstrapStatus.GateABypassed, + "development-live-gate-a-bypassed"); } if (string.IsNullOrWhiteSpace(authorizationFilePath)) @@ -305,13 +364,37 @@ internal static class DevelopmentLiveLaunchBootstrap } } + private static bool TryHasGateAEnvironment( + IDevelopmentLiveBootstrapPlatform platform, + out bool hasGateAEnvironment) + { + hasGateAEnvironment = false; + try + { + foreach (var name in GateAEnvironmentNames) + { + if (platform.GetProcessEnvironmentVariable(name) is not null) + { + hasGateAEnvironment = true; + } + } + + return true; + } + catch (Exception exception) when (IsExpectedEnvironmentFailure(exception)) + { + hasGateAEnvironment = false; + return false; + } + } + private static DevelopmentLiveBootstrapResult RejectedAndDisarm( IDevelopmentLiveBootstrapPlatform platform, string code) { - // An exact Debug request that failed validation must not fall through to + // A required Live request that failed validation must not fall through to // inherited process state from an earlier shell or Visual Studio session. - // The app also forces this requested-but-not-applied state to DryRun. + // The app treats this requested-but-not-applied state as a startup failure. for (var index = ArmedEnvironmentNames.Length - 1; index >= 0; index--) { var name = ArmedEnvironmentNames[index]; @@ -354,7 +437,7 @@ internal static class DevelopmentLiveLaunchBootstrap private static bool IsExpectedFileFailure(Exception exception) => exception is IOException or UnauthorizedAccessException or SecurityException or - ArgumentException or NotSupportedException; + InvalidDataException or ArgumentException or NotSupportedException; private static bool IsExpectedEnvironmentFailure(Exception exception) => exception is ArgumentException or SecurityException or InvalidOperationException; diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs index 5ea728e..de03211 100644 --- a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs +++ b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs @@ -88,11 +88,10 @@ public static class PlayoutOptionsLoader } /// - /// Loads the inert LocalAppData base profile for an already-applied exact - /// Debug Development Live bootstrap. This path deliberately ignores process - /// environment and ordinary playout-profile scene overrides. Live mode is - /// armed from bootstrap provenance, and the scene root is pinned to the - /// native folder selected during first-run setup. + /// Loads the protected LocalAppData Live profile for an already-applied + /// Development Live bootstrap. This path deliberately ignores process + /// environment and ordinary playout-profile scene overrides. The scene root + /// is pinned to the native folder selected during first-run setup. /// internal static PlayoutOptions LoadDevelopmentLive( string? path = null, @@ -188,7 +187,7 @@ public static class PlayoutOptionsLoader private static void EnsureDevelopmentLiveBaseContract(PlayoutOptions options) { - if (options.Mode != PlayoutMode.DryRun || + if (options.Mode != PlayoutMode.Live || !IsLiteralLoopback(options.Host) || options.Port is < 1 or > 65_535 || options.TcpMode != 1 || @@ -285,7 +284,7 @@ public static class PlayoutOptionsLoader } if (seen.Count != DevelopmentLiveProperties.Count || - !IsExactString(root, "mode", "DryRun") || + !IsExactString(root, "mode", "Live") || !IsLoopbackString(root, "host") || !IsIntegerInRange(root, "port", 1, 65_535) || !IsExactInteger(root, "tcpMode", 1) || diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs index 3a21e8a..ce0bd9c 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs @@ -17,29 +17,25 @@ public sealed class LegacyDevelopmentLiveStartupContractTests "MBN_STOCK_WEBVIEW.LegacyParityApp"); [Fact] - public void VisualStudioProfilesPutDevelopmentLiveFirstAndKeepExplicitDryRun() + public void VisualStudioExposesOneLivePackageProfileWithoutDryRun() { using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine( ParityRoot, "Properties", "launchSettings.json"))); - var profiles = document.RootElement.GetProperty("profiles") - .EnumerateObject() - .ToArray(); + var profile = Assert.Single(document.RootElement.GetProperty("profiles") + .EnumerateObject()); - Assert.Equal(2, profiles.Length); - Assert.Equal( - "MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)", - profiles[0].Name); - Assert.Equal("MsixPackage", profiles[0].Value.GetProperty("commandName").GetString()); - Assert.Equal(ExactArgument, - profiles[0].Value.GetProperty("commandLineArgs").GetString()); - Assert.Equal( - "MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)", - profiles[1].Name); - Assert.Equal("MsixPackage", profiles[1].Value.GetProperty("commandName").GetString()); - Assert.Equal(string.Empty, - profiles[1].Value.GetProperty("commandLineArgs").GetString()); + Assert.Contains("Live", profile.Name, StringComparison.Ordinal); + Assert.DoesNotContain("DryRun", profile.Name, StringComparison.Ordinal); + Assert.Equal("MsixPackage", profile.Value.GetProperty("commandName").GetString()); + var arguments = profile.Value.TryGetProperty("commandLineArgs", out var value) + ? value.GetString() + : null; + Assert.True( + string.IsNullOrEmpty(arguments) || + string.Equals(arguments, ExactArgument, StringComparison.Ordinal), + "The sole Live package profile may use only the normal launch or exact Live argument."); } [Fact] @@ -75,7 +71,7 @@ public sealed class LegacyDevelopmentLiveStartupContractTests } [Fact] - public void AppAppliesCompileTimeBuildGateBeforeConstructingMainWindow() + public void AppAppliesRuntimeGateBeforeConstructingAnyRuntimeWindow() { var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs")); var bootstrap = app.IndexOf( @@ -87,35 +83,37 @@ public sealed class LegacyDevelopmentLiveStartupContractTests var constructMainWindow = app.IndexOf( "new MainWindow();", StringComparison.Ordinal); + var sourceOnlyDirective = app.IndexOf( + "#if SOURCE_ONLY_RUNTIME", + StringComparison.Ordinal); + var fullRuntimeDirective = app.IndexOf( + "#else", + constructSetupWindow, + StringComparison.Ordinal); + var runtimeEndDirective = app.IndexOf( + "#endif", + constructMainWindow, + StringComparison.Ordinal); Assert.True(bootstrap >= 0); - Assert.True(constructSetupWindow > bootstrap); - Assert.True(constructMainWindow > constructSetupWindow); - Assert.Contains("#if DEBUG", app, StringComparison.Ordinal); - Assert.Contains("const bool isDebugBuild = true;", app, StringComparison.Ordinal); - Assert.Contains("const bool isDebugBuild = false;", app, StringComparison.Ordinal); - Assert.Contains("#if SOURCE_ONLY_RUNTIME", app, StringComparison.Ordinal); - Assert.Contains("const bool isSourceOnlyBuild = true;", app, - StringComparison.Ordinal); - Assert.Contains("const bool isSourceOnlyBuild = false;", app, - StringComparison.Ordinal); - Assert.Contains("isSourceOnlyBuild", app, StringComparison.Ordinal); - Assert.Contains("? null", app, StringComparison.Ordinal); + Assert.True(sourceOnlyDirective >= 0); + Assert.True(constructSetupWindow > sourceOnlyDirective); + Assert.True(fullRuntimeDirective > constructSetupWindow); + Assert.True(bootstrap > fullRuntimeDirective); + Assert.True(constructMainWindow > bootstrap); + Assert.True(runtimeEndDirective > constructMainWindow); Assert.Contains("args.Arguments", app, StringComparison.Ordinal); Assert.Contains("Environment.GetCommandLineArgs()", app, StringComparison.Ordinal); Assert.Contains("ResolveLaunchArguments(", app, StringComparison.Ordinal); Assert.Contains( - "IsDevelopmentLiveLaunchRequested = string.Equals(", + "IsDevelopmentLiveLaunchRequested = developmentLive.IsRequested;", app, StringComparison.Ordinal); Assert.Contains( "IsDevelopmentLiveLaunch = developmentLive.IsApplied;", app, StringComparison.Ordinal); - Assert.Contains( - "_window = showFirstRunSetup", - app, - StringComparison.Ordinal); + Assert.DoesNotContain("#if DEBUG", app, StringComparison.Ordinal); } [Fact] @@ -162,14 +160,15 @@ public sealed class LegacyDevelopmentLiveStartupContractTests " _appliedOperatorSettings.SceneDirectory", playout.ReplaceLineEndings("\n"), StringComparison.Ordinal); + Assert.DoesNotContain("forceSafeDryRun", playout, StringComparison.Ordinal); Assert.Contains( - "IsSourceOnlyBuild ||\n _isDevelopmentLiveLaunchRequested", - playout.ReplaceLineEndings("\n"), + "if (_playoutOptions.Mode != PlayoutMode.Live)", + playout, StringComparison.Ordinal); } [Fact] - public void DevelopmentLiveDatabaseFailureForcesDryRunAndSkipsAutomaticConnect() + public void DatabaseFailureDoesNotDowngradeOrBlockValidatedLiveEngine() { var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs")) .ReplaceLineEndings("\n"); @@ -178,49 +177,87 @@ public sealed class LegacyDevelopmentLiveStartupContractTests "MainWindow.Playout.cs")) .ReplaceLineEndings("\n"); - var blockAssignment = window.IndexOf( - "_developmentLiveStartupBlocked = true;", - StringComparison.Ordinal); - var successAssignment = window.IndexOf( - "databaseInitializationSucceeded = true;", - StringComparison.Ordinal); var runtimeReset = window.IndexOf( "_databaseRuntime = null;", - successAssignment, StringComparison.Ordinal); var pagePlanReset = window.IndexOf( "fixedPagePlanProvider = null;", - successAssignment, + runtimeReset, + StringComparison.Ordinal); + var databaseFailureEnd = window.IndexOf( + "_industryWorkflow = new LegacyIndustrySelectionWorkflow(", + pagePlanReset, StringComparison.Ordinal); var playoutInitialization = window.IndexOf( "InitializePlayoutRuntime();", StringComparison.Ordinal); - Assert.True(successAssignment >= 0); - Assert.True(runtimeReset > successAssignment); - Assert.True(pagePlanReset > successAssignment); - Assert.True(blockAssignment > runtimeReset); - Assert.True(playoutInitialization > blockAssignment); - Assert.Contains( - "var databaseInitializationSucceeded = false;", - window, + + Assert.True(runtimeReset >= 0); + Assert.True(pagePlanReset > runtimeReset); + Assert.True(databaseFailureEnd > pagePlanReset); + Assert.True(playoutInitialization > databaseFailureEnd); + Assert.DoesNotContain( + "_developmentLiveStartupBlocked = true;", + window[runtimeReset..databaseFailureEnd], + StringComparison.Ordinal); + Assert.DoesNotContain("databaseInitializationSucceeded", window, StringComparison.Ordinal); Assert.Contains( - "if (_isDevelopmentLiveLaunch && !databaseInitializationSucceeded)\n" + - " {\n" + - " // Development Live is one fail-closed startup transaction.", - window, - StringComparison.Ordinal); - Assert.Contains( - "_isDevelopmentLiveLaunch &&\n" + - " !_developmentLiveStartupBlocked\n" + + "_isDevelopmentLiveLaunch\n" + " ? PlayoutOptionsLoader.LoadDevelopmentLive(", playout, StringComparison.Ordinal); Assert.Contains( - "_isDevelopmentLiveLaunchRequested ||\n" + - " _developmentLiveStartupBlocked", + "if (_playoutOptions.Mode != PlayoutMode.Live)", playout, StringComparison.Ordinal); + Assert.Contains( + "_playoutEngine = PlayoutEngineFactory.Create(", + playout, + StringComparison.Ordinal); + } + + [Fact] + public void BootstrapOrPlayoutInvariantFailureBlocksEngineAndAutomaticConnect() + { + var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs")) + .ReplaceLineEndings("\n"); + var playout = File.ReadAllText(Path.Combine( + ParityRoot, + "MainWindow.Playout.cs")) + .ReplaceLineEndings("\n"); + + var initialBlock = window.IndexOf( + "_developmentLiveStartupBlocked =\n" + + " _isDevelopmentLiveLaunchRequested &&\n" + + " !_isDevelopmentLiveLaunch;", + StringComparison.Ordinal); + var earlyBlock = playout.IndexOf( + "if (_developmentLiveStartupBlocked)", + StringComparison.Ordinal); + var earlyReturn = playout.IndexOf("return;", earlyBlock, StringComparison.Ordinal); + var loadLive = playout.IndexOf( + "PlayoutOptionsLoader.LoadDevelopmentLive(", + StringComparison.Ordinal); + var requireLive = playout.IndexOf( + "if (_playoutOptions.Mode != PlayoutMode.Live)", + StringComparison.Ordinal); + var createEngine = playout.IndexOf( + "_playoutEngine = PlayoutEngineFactory.Create(", + StringComparison.Ordinal); + var blockOnInitializationFailure = playout.IndexOf( + "_developmentLiveStartupBlocked = true;", + createEngine, + StringComparison.Ordinal); + + Assert.True(initialBlock >= 0); + Assert.True(earlyBlock >= 0); + Assert.True(earlyReturn > earlyBlock); + Assert.True(loadLive > earlyReturn); + Assert.True(requireLive > loadLive); + Assert.True(createEngine > requireLive); + Assert.True(blockOnInitializationFailure > createEngine); + Assert.DoesNotContain("forceSafeDryRun", playout, StringComparison.Ordinal); Assert.Contains( "if (!_developmentLiveStartupBlocked)\n" + " {\n" + diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs index e78638b..a4a2f9f 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs @@ -16,17 +16,28 @@ public sealed class LegacyFirstRunSetupNativeContractTests var app = File.ReadAllText(Path.Combine(AppRoot, "App.xaml.cs")) .ReplaceLineEndings("\n"); - Assert.Contains( - "var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild;\n" + - " _window = showFirstRunSetup\n" + - " ? new FirstRunSetupWindow()\n" + - " : new MainWindow();", - app, + var sourceOnlyStart = app.IndexOf("#if SOURCE_ONLY_RUNTIME", StringComparison.Ordinal); - Assert.Contains( - "var launchArguments = isSourceOnlyBuild\n" + - " ? null", - app, + var fullRuntimeStart = app.IndexOf("#else", sourceOnlyStart, + StringComparison.Ordinal); + var runtimeEnd = app.IndexOf("#endif", fullRuntimeStart, + StringComparison.Ordinal); + Assert.True(sourceOnlyStart >= 0); + Assert.True(fullRuntimeStart > sourceOnlyStart); + Assert.True(runtimeEnd > fullRuntimeStart); + + var sourceOnly = app[sourceOnlyStart..fullRuntimeStart]; + var fullRuntime = app[fullRuntimeStart..runtimeEnd]; + Assert.Contains("new FirstRunSetupWindow()", sourceOnly, StringComparison.Ordinal); + Assert.DoesNotContain("new MainWindow()", sourceOnly, StringComparison.Ordinal); + Assert.DoesNotContain( + "DevelopmentLiveLaunchBootstrap.TryApply(", + sourceOnly, + StringComparison.Ordinal); + Assert.Contains("DevelopmentLiveLaunchBootstrap.TryApply(", fullRuntime, + StringComparison.Ordinal); + Assert.Contains("new MainWindow()", fullRuntime, StringComparison.Ordinal); + Assert.DoesNotContain("new FirstRunSetupWindow()", fullRuntime, StringComparison.Ordinal); Assert.DoesNotContain( "new FirstRunSetupWindow(App.IsDevelopmentLiveLaunch", diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs index 2fb05f4..e736eb4 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs @@ -104,9 +104,11 @@ public sealed class LegacyOperatorSettingsNativeContractTests Playout, StringComparison.Ordinal); Assert.Contains( - "operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory", + "operatorBackgroundDirectory:", Playout, StringComparison.Ordinal); + Assert.Contains("_appliedOperatorSettings.BackgroundDirectory", Playout, + StringComparison.Ordinal); Assert.DoesNotContain("InitializePlayoutRuntime();", Settings, StringComparison.Ordinal); Assert.DoesNotContain("CreateLegacyExecutableDefault(", Settings, StringComparison.Ordinal); diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs index 9f017f8..dd27b66 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs @@ -442,7 +442,9 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "MainWindow.Playout.cs")); - Assert.Contains("forceSafeDryRun: IsSourceOnlyBuild", playout, + Assert.DoesNotContain("forceSafeDryRun", playout, + StringComparison.Ordinal); + Assert.Contains("if (_playoutOptions.Mode != PlayoutMode.Live)", playout, StringComparison.Ordinal); } @@ -540,7 +542,8 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests "[ValidateSet('127.0.0.1', '::1')]", initializer, StringComparison.Ordinal); - Assert.Contains("mode = 'DryRun'", initializer, StringComparison.Ordinal); + Assert.Contains("mode = 'Live'", initializer, StringComparison.Ordinal); + Assert.DoesNotContain("mode = 'DryRun'", initializer, StringComparison.Ordinal); Assert.Contains( "trustedLiveOutputEnabled = $true", initializer, diff --git a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs index 7ad68f1..75566e9 100644 --- a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs @@ -2,6 +2,7 @@ using System.Security; using System.Text; using MBN_STOCK_WEBVIEW.Playout.Configuration; using MBN_STOCK_WEBVIEW.Playout.Diagnostics; +using MBN_STOCK_WEBVIEW.Playout.Safety; namespace MBN_STOCK_WEBVIEW.Playout.Tests; @@ -42,12 +43,18 @@ public sealed class DevelopmentLiveLaunchBootstrapTests } [Fact] - public void ResolveLaunchArguments_RejectsMissingOrAdditionalProcessArguments() + public void ResolveLaunchArguments_DistinguishesNormalLaunchFromAdditionalArguments() { Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments("", ["app.exe"])); - Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments( + var additionalArguments = DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments( "", - ["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, "extra"])); + ["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, "extra"]); + + Assert.NotNull(additionalArguments); + Assert.NotEqual(string.Empty, additionalArguments); + Assert.NotEqual( + DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, + additionalArguments); } [Fact] @@ -65,13 +72,11 @@ public sealed class DevelopmentLiveLaunchBootstrapTests } [Theory] - [InlineData(null)] - [InlineData("")] [InlineData("--development-live ")] [InlineData(" --development-live")] [InlineData("--DEVELOPMENT-LIVE")] [InlineData("--development-live --another-option")] - public void NonExactArgument_SkipsWithoutReadingOrMutating(string? arguments) + public void NonExactArgument_IsRejectedWithoutReadingOrMutating(string? arguments) { var platform = FakePlatform.Valid(); @@ -81,37 +86,56 @@ public sealed class DevelopmentLiveLaunchBootstrapTests "ignored.json", platform); - Assert.Equal(DevelopmentLiveBootstrapStatus.Skipped, result.Status); - Assert.Equal("development-live-not-requested", result.Code); + Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status); + Assert.Equal("development-live-arguments-invalid", result.Code); + Assert.False(result.IsLaunchAllowed); + Assert.True(result.IsRequested); Assert.Equal(0, platform.ReadCount); Assert.Empty(platform.Environment); } - [Fact] - public void ReleaseBuild_SkipsExactArgumentWithoutReadingOrMutating() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GateALaunch_BypassesOrdinaryAuthorizationWithoutEnvironmentMutation( + bool isDebugBuild) { var platform = FakePlatform.Valid(); + platform.Environment[ + PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable] = + new string('A', 64); + var originalEnvironment = platform.Environment.ToArray(); var result = DevelopmentLiveLaunchBootstrap.TryApply( - DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, - isDebugBuild: false, - "ignored.json", + null, + isDebugBuild, + "must-not-be-read.json", platform); - Assert.Equal(DevelopmentLiveBootstrapStatus.Skipped, result.Status); - Assert.Equal("development-live-debug-only", result.Code); + Assert.Equal(DevelopmentLiveBootstrapStatus.GateABypassed, result.Status); + Assert.Equal("development-live-gate-a-bypassed", result.Code); + Assert.True(result.IsLaunchAllowed); + Assert.False(result.IsRequested); Assert.Equal(0, platform.ReadCount); - Assert.Empty(platform.Environment); + Assert.Equal(originalEnvironment, platform.Environment); } - [Fact] - public void DebugExactArgumentAndStrictFile_ApplyAllProcessGates() + [Theory] + [InlineData(null, true)] + [InlineData("", true)] + [InlineData("--development-live", true)] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("--development-live", false)] + public void NormalOrExactLaunchAndStrictFile_ApplyAllProcessGates( + string? arguments, + bool isDebugBuild) { var platform = FakePlatform.Valid(); var result = DevelopmentLiveLaunchBootstrap.TryApply( - DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, - isDebugBuild: true, + arguments, + isDebugBuild, "authorization.json", platform); @@ -188,6 +212,25 @@ public sealed class DevelopmentLiveLaunchBootstrapTests Assert.Empty(platform.Environment); } + [Fact] + public void InvalidAuthorizationFileRead_IsUnavailableAndDoesNotMutateEnvironment() + { + var platform = FakePlatform.Valid(); + platform.ReadException = new InvalidDataException("sensitive authorization bytes"); + + var result = DevelopmentLiveLaunchBootstrap.TryApply( + null, + isDebugBuild: false, + "sensitive authorization path", + platform); + + Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status); + Assert.Equal("development-live-authorization-unavailable", result.Code); + Assert.False(result.IsLaunchAllowed); + Assert.DoesNotContain("sensitive", result.Code, StringComparison.OrdinalIgnoreCase); + Assert.Empty(platform.Environment); + } + [Fact] public void EnvironmentFailure_DisarmsEveryPreviousProcessValue() { @@ -196,7 +239,8 @@ public sealed class DevelopmentLiveLaunchBootstrapTests InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] = "old-native"; platform.Environment[ InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] = "old-interop"; - platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] = "DryRun"; + platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] = + "stale-mode"; platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] = "old-authorization"; platform.FailOnceOnSetName = DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable; diff --git a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs index 4d13b06..71c3018 100644 --- a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs @@ -256,7 +256,7 @@ public sealed class PlayoutOptionsLoaderTests } [Fact] - public void LoadDevelopmentLive_RejectsNonCanonicalJsonAndAllowlistExpansion() + public void LoadDevelopmentLive_RejectsWrongModeNonCanonicalJsonAndAllowlistExpansion() { using var environment = ClearedEnvironment(); var canonical = DevelopmentLiveBaseJson(); @@ -265,12 +265,16 @@ public sealed class PlayoutOptionsLoaderTests string[] invalidConfigurations = [ canonical.Replace( + "\"mode\": \"Live\"", "\"mode\": \"DryRun\"", - "\"Mode\": \"DryRun\"", StringComparison.Ordinal), canonical.Replace( - "\"mode\": \"DryRun\"", - "\"mode\": \"DryRun\", \"mode\": \"DryRun\"", + "\"mode\": \"Live\"", + "\"Mode\": \"Live\"", + StringComparison.Ordinal), + canonical.Replace( + "\"mode\": \"Live\"", + "\"mode\": \"Live\", \"mode\": \"Live\"", StringComparison.Ordinal), canonical.Replace( "\"port\": 31001", @@ -586,7 +590,7 @@ public sealed class PlayoutOptionsLoaderTests private static string DevelopmentLiveBaseJson() => """ { - "mode": "DryRun", + "mode": "Live", "host": "127.0.0.1", "port": 31001, "tcpMode": 1,