feat: verify Tornado PGM KTAP connection

This commit is contained in:
2026-07-10 14:28:45 +09:00
parent b113830c14
commit 930424b752
22 changed files with 3474 additions and 67 deletions

View File

@@ -8,10 +8,10 @@
여기의 `30001`은 원본 시스템의 당시 값일 뿐 새 Test endpoint의 기본값이나 검증값이 아닙니다. 새 설정은 격리 Test Tornado의 현재 `Tools > Option > Control > Network Server > TCP Port`를 직접 확인해 사용합니다. 반환값 `1`도 매뉴얼의 `OnHello` 또는 Network Monitoring `[R]`/`[S]` 확인을 대신하지 않습니다.
장면 PREPARE의 기준 순서는 다음과 같습니다.
원본 장면 PREPARE의 호출 순서는 다음과 같습니다.
1. `LoadScene(Cuts\<file>.t2s, <scene alias>)`
2. layout `10`에 fade effect `7``FadeInSec` 적용
2. IN effect 플래그에 fade effect `7``FadeInSec` 적용
3. `BeginTransaction()`
4. 장면별 데이터와 오브젝트 변경
5. `scene.QueryVariables()`
@@ -20,7 +20,9 @@
TAKE IN은 준비된 장면에 `Play(10)`을 호출하고 `m_TakeIn=true`로 전환합니다. TAKE OUT은 과거 `CutOut(10)` 대신 현재 운영 코드와 동일하게 `StopAll()`을 사용하고 on-air 상태를 해제합니다. NEXT는 `m_TakeIn`이 참일 때만 실행되므로 IDLE 또는 PREPARED 상태에서 바로 출력을 시작해서는 안 됩니다.
`DynamicK3dSession`위 COM 호출 순서와 layout/effect/TAKE OUT 동작을 보존합니다. `TornadoPlayoutEngine` on-air 상태가 없으면 NEXT를 COM 호출 전에 거부합니다.
`DynamicK3dSession`매뉴얼의 transaction 제한에 맞춰 오브젝트 변경을 `BeginTransaction`/`EndTransaction[OnChannel]` 안에서 끝낸 뒤 `QueryVariables`를 호출하고 `Prepare`합니다. `SetSceneEffectType`의 첫 인수도 layout이 아니라 IN effect 플래그 `1`로 전달하며, layout `10``Prepare`/`Play`/`CutOut`에만 사용합니다. `TornadoPlayoutEngine` on-air 상태가 없으면 NEXT를 COM 호출 전에 거부합니다.
K3D의 Play/Stop 계열은 완료 이벤트가 별도인 비동기 명령입니다. 현재 callback handler를 아직 포팅하지 않았으므로 이전/on-air Scene을 명령 반환 직후 `Unload`하지 않고 연결 종료까지 보존합니다. 실제 운영의 장기 세션 정리는 `OnScenePlayed`/`OnCutOut`/`OnStopAll` 성공 콜백 기반으로 구현해야 합니다.
## Scene 빌더의 범위
@@ -42,7 +44,7 @@ TAKE IN은 준비된 장면에 `Play(10)`을 호출하고 `m_TakeIn=true`로 전
- x64 COM 활성화와 KTAP 연결/해제
- 승인된 `.t2s`의 load와 scene alias
- transaction, `QueryVariables`, layout 10 prepare
- 오브젝트 transaction 종료 뒤 `QueryVariables`, layout 10 prepare
- TAKE IN, 다음 cue의 NEXT, TAKE OUT `StopAll`
- STA 직렬화, timeout, 프로세스 교체 및 오류 상태
@@ -52,3 +54,4 @@ TAKE IN은 준비된 장면에 `Play(10)`을 호출하고 `m_TakeIn=true`로 전
- `PageN`/`Nxt_PageN` 페이지 계산과 같은 scene 갱신
- 배경 영상·texture 및 그래프/path mutation
- 실제 시장 데이터와 원본 화면의 픽셀/내용 비교
- `OnScenePlayed`/`OnCutOut`/`OnStopAll` callback 기반 Scene unload

View File

@@ -54,8 +54,11 @@ WebView는 `https://app.mbn.local` 가상 호스트로 패키지 내부 파일
- 완료: 연결/해제, 명시적 재연결(no replay), `Tornado2` 접두사 프로세스 감시
- 완료: `PREPARE`, `TAKE IN`, `NEXT`, `TAKE OUT` WebView 메시지 및 상태/오류 UI 연결
- 완료: 기본 DryRun, Test의 단일 loopback 테스트 인스턴스·채널·씬 allowlist, Live 이중 승인
- 확인 대기: 현재 `PGM`과 분리된 테스트 Tornado 인스턴스·출력 채널·테스트 씬의 실제 호출
- 완료: 현재 Tornado2 PGM 렌더 창에 대한 x64 K3D `KTAPConnect → Disconnect` 실제 왕복 및 Network Monitoring `[R] HELLO`/`[S] SUCCESS HELLO` 확인. 렌더 명령은 호출하지 않음
- 완료: 네이티브/Interop 이중 SHA-256 핀, 프로세스 수명 파일 잠금, 실제 PGM listener 소유권 및 KTAP 지연 dispatch 차단
- 확인 대기: 승인된 테스트 출력에서 `5001 → 5006 → TAKE OUT` 실제 호출
- 확인 대기: 승인된 Test 환경에서 MSIX 컨텍스트의 실제 COM 활성화와 출력 관찰
- 후속: `OnScenePlayed`/`OnCutOut`/`OnStopAll` callback 기반 장기 세션 Scene unload
- 후속: 35개 scene builder의 복합 K3D mutation 및 `PageN`/`Nxt_PageN` 같은-scene 페이지 갱신 포팅
### 화면 기능

View File

@@ -34,7 +34,24 @@ powershell -NoProfile -ExecutionPolicy Bypass `
## 런타임 참조 방식
앱은 빌드 시점의 `Interop.K3DAsyncEngineLib.dll`을 참조하지 않습니다. HKLM 64비트 등록과 HKCU override 부재를 검증한 뒤 고정된 KAEngine/KAEventHandler CLSID로만 런타임 late binding하고, COM 객체와 호출 세부 사항은 `IPlayoutEngine` 구현 안에 격리합니다. 따라서 `obj` 폴더에 남은 Interop DLL 또는 특정 개발 장비의 벤더 설치 경로가 빌드 입력이 되지 않습니다.
앱은 빌드 시점의 `Interop.K3DAsyncEngineLib.dll`을 참조하거나 패키징하지 않습니다. 런타임에는 HKLM Registry64의 TypeLib·KAEngine·KAEventHandler 세 등록이 모두 같은 `DLL\x64\Release\K3DAsyncEngine.dll`을 가리키고 HKCU override가 없을 때만, 그 벤더 루트의 정확한 `Bin\x64\C#\Interop.K3DAsyncEngineLib.dll` 하나를 사용합니다. 상위 폴더 검색이나 `obj`/`artifacts` fallback은 없습니다. 두 파일과 모든 상위 경로는 reparse point가 아니어야 하며 x64 PE인지 확인합니다. COM 객체와 호출 세부 사항은 `IPlayoutEngine` 구현 안에 격리합니다.
등록된 네이티브 `K3DAsyncEngine.dll`과 설치된 C# Interop은 모두 로드 전에 운영자가 승인한 SHA-256과 일치해야 합니다. 현재 프로세스 환경의 `MBN_STOCK_K3D_NATIVE_SHA256``MBN_STOCK_K3D_INTEROP_SHA256`에 각각 공백 없는 64자리 16진수만 허용하며, 일반 Test/Live 어댑터와 PGM 연결 전용 진단이 같은 두 핀을 사용합니다. 먼저 벤더 배포 해시, 신뢰된 설치 매체 또는 관리자 검수로 **두 파일을 독립적으로 승인한 뒤** 그 정확한 파일들을 대상으로 다음 값을 설정합니다.
```powershell
$approvedNative = '<vendor-or-admin-approved-x64-native-file>'
$approvedInterop = '<vendor-or-admin-approved-x64-interop-file>'
$nativeHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $approvedNative).Hash
$interopHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $approvedInterop).Hash
if ($nativeHash -notmatch '^[0-9A-Fa-f]{64}$' -or
$interopHash -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Invalid SHA-256.' }
$env:MBN_STOCK_K3D_NATIVE_SHA256 = $nativeHash
$env:MBN_STOCK_K3D_INTEROP_SHA256 = $interopHash
```
검수하지 않은 현재 설치 파일의 해시를 계산해 그대로 승인하는 것은 독립적인 신뢰 확인이 아닙니다. 파일 업데이트 후 어느 핀이든 바꾸는 행위는 새 바이너리를 신뢰한다는 명시적 운영 결정이므로 변경 이력과 승인 근거를 남기고, 위 `$env:`를 설정한 같은 셸에서 앱 또는 진단을 시작합니다. 핀과 설치 경로는 결과 JSON이나 로그에 출력하지 않습니다. 핀이 없거나 형식·해시·경로·PE·Interop 메타데이터 검사가 하나라도 실패하면 `installed-interop-metadata-unavailable`로 COM 활성화 전에 종료합니다. 검사를 통과한 두 파일은 프로세스 수명 동안 쓰기·교체 공유를 허용하지 않는 읽기 핸들로 유지합니다.
이 이중 핀과 장기 파일 잠금은 개발 워크스테이션에서 우발적 교체 및 검사-사용 시점(TOCTOU) 교체를 완화하는 방어입니다. 운영 장비에서는 이것만으로 충분하지 않으며, 벤더 설치 루트 ACL을 `Administrators`/`SYSTEM`만 쓰기 가능하고 일반 `Users`는 읽기·실행만 가능하도록 배포 단계에서 구성해야 합니다. 또한 서명된 벤더 패키지 또는 관리자가 별도로 승인한 배포물을 사용해야 합니다. 앱과 진단은 ACL을 수정하거나 취약한 설치를 자동으로 신뢰하지 않습니다.
타입 정보를 조사해야 할 때만 [Generate-K3DInterop.ps1](../scripts/Generate-K3DInterop.ps1)을 선택적으로 사용합니다. 생성 전 Inspect와 같은 HKLM 양방향 매핑·두 COM 서버 AMD64·HKCU override 부재 검사를 통과해야 합니다. 그 뒤 알려진 Windows SDK의 x64 `TlbImp.exe` 위치를 선택하거나 명시적으로 받은 x64 경로를 검증하고 `/machine:X64`로 생성합니다. 입력 TypeLib과 도구 및 결과물 모두 AMD64인지 확인하며, 결과는 Git에서 제외된 다음 위치에만 씁니다.
@@ -56,7 +73,7 @@ powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Generate-K3DInterop.ps1
```
SDK가 기본 위치에 없다면 x64 SDK의 `TlbImp.exe` 절대 경로를 `-TlbImpPath`에 지정합니다. 생성물은 진단용일 뿐 프로젝트 참조나 MSIX 콘텐츠로 추가하지 않습니다. 이 스크립트도 COM 활성화와 등록 변경은 하지 않습니다.
SDK가 기본 위치에 없다면 x64 SDK의 `TlbImp.exe` 절대 경로를 `-TlbImpPath`에 지정합니다. 생성물은 타입 조사용일 뿐 프로젝트 참조나 MSIX 콘텐츠로 추가하지 않습니다. 런타임 resolver도 이 `artifacts` 생성물을 찾거나 로드하지 않습니다. 이 스크립트도 COM 활성화와 등록 변경은 하지 않습니다.
## 로컬 설정
@@ -116,6 +133,28 @@ Tornado2의 `View > Network Monitoring Window`에서 `[R]`은 서버가 클라
상태의 `accepted-unconfirmed``KTAPConnect`가 SDK 성공값 `1`을 반환했다는 뜻일 뿐입니다. 매뉴얼 41쪽의 `OnHello` 콜백이나 실제 `[R]`/`[S]`를 자동 확인했다는 뜻이 아닙니다. 현재 late-bound 어댑터는 282개 메서드 `IKAEventHandler` ABI를 안전하게 패키징하는 검증된 전략이 없어 `ktapHelloObserved``null`로 보고합니다. `lastKtapConnectState`는 현재 연결 상태가 아니라 가장 최근 KTAP dispatch 시도의 증거이며, 화면은 `Connected`/`Faulted` 같은 현재 상태와 분리해 표시합니다. 따라서 격리 `--test-connect`가 성공했는데도 같은 시각의 `[R]`/`[S]`가 전혀 없다면 `--test-sequence`로 진행하지 말고 mode/config 파일, 실제 Network Server TCP Port와 안전 게이트 거부 여부를 먼저 확인합니다.
### PGM 네트워크 연결 전용 진단
Tornado2의 `PGM` 창은 본 프로그램이 보낸 KTAP 송출 명령을 표시하는 렌더 출력 창입니다. 창 제목만으로 실제 방송 라우팅 여부를 판정할 수 없으므로 기존 Test 게이트에서 PGM을 허용하지 않습니다. 대신 운영자가 Network Monitoring 창을 먼저 열고 승인한 회차에만 다음 별도 명령으로 네트워크 세션 왕복을 확인합니다.
```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
-c Debug -p:Platform=x64 -- `
--pgm-connect-diagnostic `
--i-understand-this-will-contact-current-pgm-tornado-via-ktap-connect-and-disconnect-only `
--host 127.0.0.1 `
--port <Tornado-Network-Server-TCP-Port> `
--expected-pgm-window-title PGM
```
이 명령은 설정 파일과 `MBN_STOCK_PLAYOUT_*` 환경 override를 받지 않으며 host는 숫자형 loopback만 허용합니다. 정확히 하나의 `Tornado2*` 프로세스, 지정한 PGM 창 제목, 프로세스 세대와 해당 TCP LISTEN 소유권을 COM 전·KTAP 직전·Disconnect 직전에 반복 확인합니다. 자동 재연결과 재시도는 없고 연결·해제 timeout은 각각 3초로 고정됩니다.
COM 경로에는 `KTAPConnect`와 성공 후 최대 한 번의 `Disconnect`만 있으며 `GetScenePlayer[OnChannel]`, `LoadScene`, `Prepare`, `Play`, `PlayDirect`, `CutOut`, `Stop`, `StopAll`을 호출할 수 있는 API 표면이 없습니다. 따라서 명시적인 장면/레이어 출력 명령은 발생하지 않습니다. 다만 PGM에 TCP 제어 세션을 추가하므로 완전한 무영향을 보장하는 명령은 아니며, timeout이나 대상 변경 후에는 반복하지 않습니다.
성공 JSON의 `lastKtapConnectState: "accepted-unconfirmed"`, `completed: true`, `outcomeUnknown: false`, `renderCommandSurfaceExposed: false`, `renderCommandAttempted: false`를 확인하고 같은 시각의 Network Monitoring `[R] HELLO``[S] SUCCESS HELLO`를 사람이 확인합니다. `GetScenePlayer`를 호출하지 않으므로 이 진단의 성공은 장면 송출 준비 완료를 의미하지 않습니다.
x64 SDK의 네이티브 DLL과 정식 Interop은 위의 Registry64 단일 경로·reparse ancestry·AMD64 PE·운영자별 SHA-256 핀을 모두 통과해야 합니다. 두 파일을 쓰기/교체할 수 없게 연 핸들은 COM 활성화와 사용을 포함한 프로세스 수명 동안 유지합니다. Interop 로드 후에는 assembly/TypeLib 버전, COM import GUID와 허용 메서드 서명도 확인합니다. vendor DLL은 Git 또는 MSIX에 복사하지 않습니다.
## 모드와 안전 게이트
`Disabled`는 모든 송출 명령을 거부하는 운영 롤백 모드입니다. `DryRun`은 COM 없이 WebView 동작을 성공 결과로 모의합니다. `Test``Live`만 등록된 COM을 사용할 수 있습니다.
@@ -206,7 +245,7 @@ dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
--config $config
```
마지막으로 격리된 Test 모니터를 관찰하면서 실제 시퀀스를 실행합니다. `--observe-ms``TAKE IN` 뒤와 `NEXT` 뒤에 각각 적용되는 필수 관찰 시간이며 1,000~30,000ms만 허용합니다.
마지막으로 격리된 Test 모니터를 관찰하면서 실제 시퀀스를 실행합니다. `--observe-ms``TAKE IN`, `NEXT`, `TAKE OUT` 뒤에 각각 적용되는 필수 관찰 시간이며 1,000~30,000ms만 허용합니다.
```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
@@ -219,7 +258,9 @@ dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
--observe-ms 5000
```
시퀀스는 `Connect → Prepare(5001) → TakeIn → 관찰 → Next(5006) → 관찰 → TakeOut(All) → Disconnect` 순서입니다. 자동 재연결은 CLI가 강제로 비활성화합니다. 성공한 `TakeIn` 뒤 관찰 취소처럼 결과가 확정된 중단이면 `TakeOut(All)`을 한 번만 정리 단계로 요청한 뒤, 정리가 성공한 경우에만 `Disconnect`합니다. 이미 실행 결과가 불명확하거나 `TakeOut`이 어떤 비성공 결과라도 반환하면 출력이 남아 있을 수 있으므로 추가 출력 명령과 SDK `Disconnect`를 보내지 않습니다. 이때 `QuarantineAsync`가 같은 STA에서 제어 메서드 호출 없이 로컬 COM 참조만 해제한 다음 bounded 어댑터 폐기를 수행합니다. quarantine 자체를 완료하지 못하면 의도하지 않은 Disconnect보다 로컬 누수를 택해 일반 Dispose도 생략합니다. 어느 경우든 격리 모니터에서 최종 Test 출력 상태를 사람이 확인해야 합니다.
시퀀스는 `Connect → Prepare(5001) → TakeIn → 관찰 → Next(5006) → 관찰 → TakeOut(All) → 관찰 → Disconnect` 순서입니다. K3D의 Play/Stop 완료는 비동기 이벤트이므로 마지막 관찰 창이 끝나기 전에는 Disconnect하지 않습니다. 자동 재연결은 CLI가 강제로 비활성화합니다. 성공한 `TakeIn` 뒤 관찰 취소처럼 결과가 확정된 중단이면 `TakeOut(All)`을 한 번만 정리 단계로 요청한 뒤, 정리가 성공한 경우에만 `Disconnect`합니다. 이미 실행 결과가 불명확하거나 `TakeOut`이 어떤 비성공 결과라도 반환하면 출력이 남아 있을 수 있으므로 추가 출력 명령과 SDK `Disconnect`를 보내지 않습니다. 이때 `QuarantineAsync`가 같은 STA에서 제어 메서드 호출 없이 로컬 COM 참조만 해제한 다음 bounded 어댑터 폐기를 수행합니다. quarantine 자체를 완료하지 못하면 의도하지 않은 Disconnect보다 로컬 누수를 택해 일반 Dispose도 생략합니다. 어느 경우든 격리 모니터에서 최종 Test 출력 상태를 사람이 확인해야 합니다.
현재 런타임은 vendor의 282-method event handler를 managed callback으로 아직 소비하지 않습니다. 따라서 `Play`, `CutOut`, `StopAll`의 COM 반환 직후 Scene을 `Unload`하지 않고 prepared/current/retired 참조를 연결 종료까지 보존합니다. 이는 두 장면만 쓰는 bounded 검증에서 조기 Unload로 화면 전환을 끊는 위험을 피하기 위한 동작입니다. 장시간 운영에서 Scene을 누적하지 않으려면 `OnScenePlayed`/`OnCutOut`/`OnStopAll` 성공 콜백 뒤에만 unload queue를 비우는 handler 포팅이 선행되어야 합니다.
JSON 결과는 단계별 operation/result code와 `connectRequestIssued`, nullable `comActivationAttempted`, `lastKtapConnectState`, `ktapConnectAttempted`, nullable `ktapConnectAccepted`, nullable `ktapHelloObserved`, nullable `networkMonitoringRecordExpected`, `networkMonitoringCheckRequired`, nullable `networkMonitoringVerified`, `outputMayBeActive`, quarantine 시도·완료 여부를 제공합니다. `connectRequestIssued`는 엔진 API 요청일 뿐 KTAP 통신 증거가 아니며, `comActivationAttempted`도 COM 활성화 추정값일 뿐입니다. `lastKtapConnectState``not-attempted`, `attempted`, `accepted-unconfirmed`, `failed` 중 하나입니다. `networkMonitoringRecordExpected`는 성공값을 받은 경우 `true`, dispatch가 없으면 `false`, local reflection/COM 실패 또는 timeout으로 서버 도달을 예측할 수 없으면 `null`입니다. `networkMonitoringCheckRequired`는 KTAP dispatch 경로에 들어간 모든 경우 `true`이며, `networkMonitoringVerified`는 앱이 Tornado2 UI를 판독하지 않으므로 항상 `null`입니다. 운영자가 직접 `[R]`/`[S]`를 확인해야 합니다. `outputMayBeActive: true`이면 자동 정리를 성공으로 확인하지 못했으므로 사람이 격리 출력을 확인해야 합니다. 특히 `Unavailable`, 취소, timeout은 연결 전 거부와 연결 도중 안전 게이트 변화가 같은 결과 code가 될 수 있으므로 추측하지 않습니다. 로컬 경로, 씬 code, PID, 창 제목, HRESULT 및 엔진 원문 오류는 출력하지 않습니다. `--test-plan``runtimeProcessGateChecked: false`는 자산 계획만 검증했다는 뜻이며 실제 연결 가능성을 증명하지 않습니다.