fix: initialize selected runtime for live playout

This commit is contained in:
2026-07-28 20:44:20 +09:00
parent 18d40e892f
commit 818acad646
24 changed files with 906 additions and 391 deletions

6
.vsconfig Normal file
View File

@@ -0,0 +1,6 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.Universal"
]
}

View File

@@ -55,7 +55,8 @@ Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑
## Visual Studio 2026에서 실행
상사 PC처럼 기존 코더 자산과 K3D가 설치된 개발 PC에서는 별도 PowerShell 명령을 먼저 실행할
필요가 없습니다.
필요가 없습니다. 솔루션 루트의 `.vsconfig`가 요구하는 **WinUI 애플리케이션 개발** 워크로드가
없으면 Visual Studio의 설치 안내를 먼저 완료하고 Visual Studio를 다시 시작합니다.
1. `MBN_STOCK_WEBVIEW.sln`을 엽니다.
2. 솔루션 구성을 `Debug`, 플랫폼을 `x64`로 선택합니다.
@@ -85,34 +86,42 @@ Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중
- `Cuts`: 실제 실행 자산의 `Cuts` 폴더
- `설정/INI (Res)`: 기존 코더가 사용하는 실제 `Res` 폴더
첫 실행 설정은 두 폴더 안의 파일을 열거·파싱·복사하지 않습니다. 폴더 경로만
첫 실행 설정은 선택한 `Res`의 정확한 `MmoneyCoder.ini`에 Oracle/MariaDB 설정이 모두
있는지 검증합니다. 파일을 다른 위치나 빌드 출력에 복사하지 않으며, 이후 전체 앱은 저장된
`Res\MmoneyCoder.ini`를 매 실행 직접 사용합니다. 두 폴더 경로는
`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`에 저장하고,
같은 공통 부모를 Git 제외 `Directory.Build.local.props`에 기록합니다.
두 폴더는 같은 공통 부모 아래에 나란히 있어야 하며 폴더 이름도 각각 `Cuts`, `Res`여야
합니다. 소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더가 아니라 기존 코더가 실제
실행에 사용하던 `bin\Debug\Cuts`, `bin\Debug\Res` 성격의 폴더를 선택합니다.
`설정 시작`PowerShell, `dotnet build`, DB 복사, K3D 검사 또는 Tornado2/PGM 작업을
실행하지 않으므로 즉시 완료되어야 합니다. 전체 앱 빌드는 창을 닫고 누르는 다음 F5에서
진행됩니다. 이때도 `Cuts`는 복사하지 않고 선택한 원래 경로를 직접 사용합니다.
`설정 시작`저장소의 검증된 초기화기를 실행해 `C:\K3DAsyncEngine`의 x64 파일과
Registry64 등록, 지속 K3D pin, `127.0.0.1:30001` Development Live 승인 및 Debug x64
빌드를 한 번에 확인하므로 시간이 걸릴 수 있습니다. 이 과정은 DB 연결, K3D COM 활성화,
Tornado2/PGM 연결이나 송출 명령을 실행하지 않습니다. 성공 뒤 누르는 다음 F5에서는
`Cuts``Res\MmoneyCoder.ini`를 복사하지 않고 선택한 원래 경로를 직접 사용합니다.
Development Live용 DB overlay, K3D pin과 승인 파일이 아직 없는 PC는 별도의
`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 전체 앱 기동이
차단됩니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다.
K3D는 다음 회사 표준 배치만 허용합니다.
- native: `C:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll`
- Interop: `C:\K3DAsyncEngine\Bin\x64\C#\Interop.K3DAsyncEngineLib.dll`
Registry64의 TypeLib, KAEngine, KAEventHandler가 모두 위 native DLL을 가리키지 않거나 파일·
AMD64·해시 검증이 실패하면 설정 완료를 표시하지 않습니다.
초기화가 성공하면 Visual Studio에서 `Debug|x64`, `Legacy Parity App (VS F5)`,
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작합니다.
앱이나 빌드 출력이 `SourceOnly` 또는 Live 기동 차단이라고 표시되면 송출을 시도하지 말고
Development Live 준비 상태를 확인합니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라
`5001` 하나로 진행합니다. Debug와 Release의 일반 실행은 모두 같은 보호된 Live
부트스트랩을 사용합니다. 이 프로필은 보호된 LocalAppData 설정 읽고 상속된
`MBN_STOCK_PLAYOUT_*`·DB 환경 변수를 무시합니다. scene root는 첫 실행에서 저장한 원래
`Cuts` 경로로 고정하며 strict
부트스트랩을 사용합니다. 이 프로필은 보호된 LocalAppData 송출 설정 읽고 상속된
`MBN_STOCK_PLAYOUT_*`·DB 환경 변수를 무시합니다. scene root와 DB INI는 첫 실행에서 저장한
원래 `Cuts`, `Res\MmoneyCoder.ini` 경로로 고정하며 strict
검증이 실패하면 Live 엔진을 만들지 않고 명확한 기동 오류를 표시합니다.
같은 clone에서는 로컬 연결 파일이 Git에 덮어써지지 않으므로 이후에는 `git pull` 후 F5로
확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB 폴더 연결이 바뀌면 첫 실행
설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 경로 저장을 반복해도 자동
설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 자동 설정을 반복해도 자동
재승인되지 않습니다. 벤더 변경을 독립적으로 검증한 뒤 두 SHA-256을 명시해 수동 교체해야
합니다. pull 뒤 Required 자산 검증이 실패하는 경우에도 F5를 계속 시도하지 말고 설정 오류를
바로잡습니다.
@@ -176,11 +185,12 @@ Release MSIX는 전체 런타임 자산을 필수로 검증합니다. 원본 위
원본 `Res/MmoneyCoder.ini`의 값은 저장소에 복사하지 않았습니다. 전체 자산 Debug 빌드,
소스 전용 빌드, 게시와 MSIX 모두 이 자격증명 파일을 빌드 입력이나 출력으로 사용하지 않습니다.
전체 자산 Debug/MSIX는 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini` 또는 기존
`database.local.json`을 사용하고, 소스 전용 빌드는 DB 미연결을 강제합니다. 일반 실행에서는
환경 변수가 선택된 파일보다 우선하지만, 검증된 Development Live 프로필은 이 override를
의도적으로 무시하고 위 LocalAppData 파일만 사용합니다. 설정 우선순위, 보안과 실제 DB 스모크 방법은
[DB 운영 가이드](docs/DATABASE.md)에 정리했습니다.
검증된 Development Live는 저장된 `Res` 경로가 있으면 그 폴더의 정확한
`MmoneyCoder.ini`를 매 실행 직접 읽고 DB 환경 변수를 무시합니다. 파일이 없거나 안전·형식
검증에 실패해도 LocalAppData 설정으로 대체하지 않고 앱의 Live 엔진 및 자동 연결을
차단합니다. 아직 선택된 `Res`가 없는 기존 설치에서만 보호된 LocalAppData INI→JSON 순서를
사용하며, 소스 전용 빌드는 DB 미연결을 강제합니다. 설정 우선순위, 보안과 실제 DB 스모크
방법은 [DB 운영 가이드](docs/DATABASE.md)에 정리했습니다.
MSIX 설치 폴더는 읽기 전용입니다. 전체 자산 빌드에서는 읽기 전용 원본 `Cuts` 179개와 허용된
UI용 `Res` 파일이 실행 출력·게시·MSIX에 포함되지만, DB 자격증명 INI·백업·인증서·벤더 DLL은

View File

@@ -18,23 +18,32 @@ Oracle ODP.NET Core 23 계열은 TAP 기반 `OpenAsync`/명령 비동기를 지
## 런타임 설정
원본 호환 앱은 호환성을 위해 다음 순서로 DB 설정을 선택합니다.
일반 원본 호환 실행은 다음 순서로 DB 설정을 선택합니다.
1. 실행 파일 옆 `Res\MmoneyCoder.ini` — 기존 수동 배치와의 호환 경로
2. `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini`MSIX용 로컬 overlay
3. 기존 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\database.local.json` — INI가 없을 때 fallback
1. 앱 설정에서 저장한 `Res` 폴더의 정확한 `MmoneyCoder.ini`
2. 실행 파일 옆 `Res\MmoneyCoder.ini`기존 수동 배치와의 호환 경로
3. `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini` — MSIX용 로컬 overlay
4. 기존 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\database.local.json` — INI가 없을 때 fallback
공식 Debug/Release 빌드와 개발 인수용 런타임 묶음은 첫 번째 파일을 생성하거나 복사하지
않습니다. 깨끗한 개발 PC에서는 두 번째 또는 세 번째 사용자 전용 경로만 사용합니다.
`MmoneyCoder.ini`가 들어 있는 원본 `Res` 전체를 빌드 출력이나 전달용 ZIP으로 복사하지
마세요.
공식 Debug/Release 빌드와 개발 인수용 런타임 묶음은 자격증명 INI를 생성하거나 복사하지
않습니다. `MmoneyCoder.ini`가 들어 있는 원본 `Res` 전체를 빌드 출력이나ZIP으로
복사하지 마세요.
Development Live에서는 저장된 운영자 `Res` 경로가 있으면 그 폴더의 정확한
`MmoneyCoder.ini`를 매 실행 직접 읽어 DB 권위 설정으로 사용합니다. 선택된 INI가 없거나
형식·필수 값이 올바르지 않으면 LocalAppData INI/JSON으로 대체하지 않고 시작을 차단합니다.
선택된 `Res` 경로가 없을 때만 LocalAppData INI와 JSON 순서로 확인합니다. Development
Live에서는 두 경우 모두 DB 환경 변수 override를 무시합니다. 별도 Gate A 검증 실행은
검증 계획에 고정된 JSON만 사용합니다.
`MmoneyCoder.ini``[Oracle]`/`[Maria]` 섹션에서 `ConnectionName=host:port/service`,
`ID`, `Pass`를 읽습니다. BOM 없는 UTF-8과 CP949를 지원하고 환경 변수는 선택된 파일 값보다
우선합니다. 원본 `Res`의 복사본·`.bak`·`.zip`은 사용하지도 배포하지도 않습니다.
`ID`, `Pass`를 읽습니다. BOM 없는 UTF-8과 CP949를 지원합니다. 일반 실행의 환경 변수는
선택된 파일 값보다 우선하지만 Development Live에서는 적용되지 않습니다. 원본 `Res`
복사본·`.bak`·`.zip`은 사용하지도 배포하지도 않습니다.
MSIX 설치 폴더는 읽기 전용이며 평문 자격증명을 패키지에 포함하지 않습니다. 패키지에서
두 번째 또는 세 번째 경로를 사용합니다. JSON 기본 경로는 다음과 같습니다.
MSIX 설치 폴더는 읽기 전용이며 평문 자격증명을 패키지에 포함하지 않습니다. 패키지는
저장된 외부 `Res` 경로나 사용자 전용 LocalAppData 설정을 사용합니다. JSON 기본 경로는
다음과 같습니다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\database.local.json

View File

@@ -54,28 +54,36 @@ SHA-256을 이 PC의 최초 기준으로 자동 고정한다. 이후 DLL이나
PowerShell 명령은 필요 없다.
1. 저장소를 clone하고 `MBN_STOCK_WEBVIEW.sln`을 Visual Studio 2026에서 연다.
2. `Debug|x64`, `Legacy Parity App (VS F5)`,
2. 솔루션 루트의 `.vsconfig` 안내에 따라 **WinUI 애플리케이션 개발** 워크로드를
설치한다. 설치 뒤에는 Visual Studio를 다시 시작한다.
3. `Debug|x64`, `Legacy Parity App (VS F5)`,
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택한다.
3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB와 송출 엔진을
4. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB와 송출 엔진을
만들지 않고 Tornado2/PGM에 연결하지 않는다.
4. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다.
5. `Res 폴더 선택`에서 기존 코더가 사용하는 실제 `Res` 폴더를 선택한다.
6. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다.
7. `설정 완료`가 표시되면 창을 닫고 F5를 한 번 더 누른다.
5. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다.
6. `Res 폴더 선택`에서 기존 코더가 사용하는 실제 `Res` 폴더를 선택한다.
7. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다.
8. Debug x64 빌드까지 끝나 `설정 완료`가 표시되면 창을 닫고 F5를 한 번 더 누른다.
`GetLatestMSVCVersion`이 Visual Studio의 `VC\Tools\MSVC` 폴더를 찾지 못하면 WinUI
워크로드가 빠진 상태이므로 2단계를 완료한 뒤 다시 연다. 이 문제는 K3D 또는 DB 연결
오류가 아니다.
소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더를 선택하지 않는다. `설정 시작`
두 폴더의 고정 로컬 경로만 다음 위치에 저장한다.
선택한 `Res\MmoneyCoder.ini`의 Oracle/MariaDB 형식을 먼저 확인하고 두 폴더의 고정 로컬
경로를 다음 위치에 저장한다.
- `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`
- 저장소의 Git 제외 `Directory.Build.local.props`
이 버튼은 폴더 안의 파일을 열거·파싱·복사하지 않으며 PowerShell, `dotnet build`, DB 복사,
K3D 검사, Live 승인 발급 또는 Tornado2/PGM 명령을 실행하지 않는다. 따라서 경로 저장은
즉시 완료되어야 한다. `Cuts``Res`의 실제 파일 검사는 다음 F5의 빌드 또는 해당 기능을
사용하는 시점에 필요한 파일 단위로 수행된다.
이 버튼은 검증된 `Initialize-ExistingDevelopmentPc.ps1`을 실행해
`C:\K3DAsyncEngine`의 x64 파일과 Registry64 등록, 최초 지속 K3D pin,
`127.0.0.1:30001` Development Live 승인 및 전체 Debug x64 빌드를 확인한다. DB INI를
복사하지 않으며 DB 연결, K3D COM 활성화, Tornado2/PGM 연결 또는 송출 명령도 실행하지
않는다. 자동 pin은 검증한 최초 설치에만 가능하고 기존 pin과 다른 DLL을 재승인하지 않는다.
전체 Debug x64 빌드는 설정 창을 닫고 누르는 다음 F5에서 진행된다. 필요하면 같은 조건을
별도 MSBuild 인자나 환경 변수 없이 다음 명령으로 확인할 수 있다.
자동 설정 안에서 전체 Debug x64 빌드를 이미 검증한다. 필요하면 설정 완료 뒤 같은 조건을
별도 MSBuild 인자나 환경 변수 없이 다음 명령으로 다시 확인할 수 있다.
```powershell
dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj `
@@ -84,39 +92,38 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar
```
출력에 외부 runtime 경로가 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드가 계속
표시되면 Visual Studio에서 다시 빌드한다. Live 기동이 차단되면 별도의 Development Live
승인이나 보호 설정이 아직 유효하지 않은 것이다.
표시되면 Visual Studio에서 다시 빌드한다. Live 기동이 차단되면 선택한 DB INI, K3D pin,
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에서는 아래 수동 초기화 절차를 별도로
완료하기 전까지 전체 앱 기동을 차단한다. K3D DLL 변경은 경로 저장을 반복해도 자동
승인되지 않는다. 최초 실제 동작
있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 설정을 다시 진행한다. K3D DLL 변경은
자동 설정을 반복해도 자동 승인되지 않는다. 최초 실제 동작
확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다.
### Development Live까지 준비할 때
### 자동 설정이 실패했거나 로컬 설정을 명시적으로 교체할 때
첫 실행 경로 저장과 별개로 DB overlay, K3D pin과 Development Live 승인까지 새로
준비해야 할 때 저장소 루트에서 다음 수동 명령을 사용한다. 이 명령은 전체 Debug 빌드
검증까지 수행하므로 단순 폴더 저장 버튼보다 오래 걸리는 것이 정상이다.
화면을 열 수 없거나 기존 runtime/Live 설정의 명시적 교체가 필요한 경우에만 저장소
루트에서 다음 수동 명령을 사용한다. `-SkipDatabaseProfile`은 선택한
`Res\MmoneyCoder.ini`를 복사하지 않고 런타임이 계속 직접 사용하게 한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Initialize-ExistingDevelopmentPc.ps1 `
-LegacyRuntimeSourceRoot '<Cuts와 Res의 공통 부모>' `
-NoFolderPicker `
-SkipDatabaseProfile `
-ConfigureDevelopmentLive `
-PlayoutHost 127.0.0.1 `
-PlayoutPort 30001 `
-PinRegisteredK3D
```
기존 로컬 runtime, DB 또는 Live 설정과 다르면 자동으로 덮어쓰지 않는다. 변경 대상을 검토한
뒤에만 `-ReplaceRuntimeBinding`, `-ReplaceDatabaseProfile`, `-ReplaceLiveConfig` 중 필요한
항목을 명시한다. 수동 모드는 기존처럼 독립 승인된 `-NativeSha256``-InteropSha256`
사용할 수도 있으며, 이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 스크립트
첫 실행 화면에서 자동으로 호출되지 않는다.
기존 로컬 runtime 또는 Live 설정과 다르면 자동으로 덮어쓰지 않는다. 오류와 변경 대상을
검토한 뒤에만 `-ReplaceRuntimeBinding`, `-ReplaceLiveConfig` 중 필요한 항목을 명시한다.
수동 모드는 기존처럼 독립 승인된 `-NativeSha256``-InteropSha256` 사용할 수도 있으며,
이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 첫 실행 화면도 같은 스크립트를 위의
고정 인수로 호출하지만 기존 값을 임의로 교체하지 않는다.
## 3. 자산이 없는 PC 대안: 검증된 Git 밖 runtime bundle 설치
@@ -208,31 +215,26 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar
## 4. DB 로컬 설정 확인 또는 대안
2단계의 기존 PC 초기화는 검증한 `MmoneyCoder.ini`를 다음 실행 사용자 전용 경로에 이미
복사한다.
2단계의 기존 PC 자동 설정은 선택한 `Res\MmoneyCoder.ini`에 필요한 Oracle/MariaDB 항목이
있는지 확인하지만 파일을 복사하지 않는다. 전체 앱은
`runtime-folders.local.json`에 저장된 `resourceDirectory`의 정확한
`MmoneyCoder.ini`를 매 실행 직접 읽는다. DB 환경 변수는 적용하지 않으며 파일 누락, reparse
point, 비고정 드라이브 또는 INI 형식 오류가 있으면 LocalAppData 설정으로 대체하지 않고
Live 엔진 생성과 자동 연결을 차단한다. 값은 화면 캡처나 로그에 남기지 않는다.
다음 기존 사용자 전용 INI와 JSON은 **저장된 `resourceDirectory`가 없는 설치에서만**
호환 fallback으로 사용한다. 3단계 외부 bundle처럼 DB 파일을 의도적으로 포함하지 않고
운영자 `Res` 경로도 저장하지 않은 흐름에서는 사용자 전용 INI 또는 JSON을 별도로 준비한다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini
```
파일 내용의 원본 일치 여부와 현재 Windows 사용자 전용 ACL을 확인하되 값을 화면 캡처나 로그에
남기지 않는다. 기존 파일이 원본과 다르면 초기화기는 자동으로 덮어쓰지 않는다. 대상이 정확한지
확인한 뒤 2단계 명령에 `-ReplaceDatabaseProfile`을 추가한다. 이 INI는 빌드 입력·출력이나
runtime bundle에 포함되지 않는다.
3단계의 외부 bundle은 DB 파일을 의도적으로 포함하지 않으므로 그 흐름에서는 다음 JSON
초기화가 필요하다. 기존 INI를 사용하지 않고 새 JSON 설정으로 전환하기로 명시적으로 결정한
경우에는 **아래 INI overlay가 존재하지 않는 것을 먼저 확인한 뒤에만** 2단계 초기화에
`-SkipDatabaseProfile`을 주고 같은 대안을 사용한다. 이 옵션은 기존 INI를 삭제하거나
비활성화하지 않으며, INI가 남아 있으면 앱이 JSON보다 먼저 사용한다. 기존 overlay가 있는
PC의 전환은 이 절차에서 임의 삭제하지 말고 별도 검토된 자격증명 제거·전환 작업으로 처리한다.
JSON 경로는 다음과 같다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\database.local.json
```
저장소 루트에서 초기화 스크립트를 사용한다. Oracle이 service name을 쓰는 환경이면
기존 INI를 사용하지 않고 JSON 설정으로 전환하기로 명시적으로 결정한 경우에는 사용자 전용
INI가 존재하지 않는 것을 먼저 확인한다. 기존 파일의 제거·전환은 이 절차에서 임의로 수행하지
않고 별도 검토된 자격증명 작업으로 처리한다. JSON을 만들려면 저장소 루트에서 초기화
스크립트를 사용한다. Oracle이 service name을 쓰는 환경이면
`-OracleSid` 대신 `-OracleServiceName`을 사용한다.
```powershell
@@ -255,21 +257,25 @@ powershell -NoProfile -ExecutionPolicy Bypass `
## 5. K3D 설치, 라이선스와 기준 해시 확인
벤더 절차로 Tornado2/K3D x64와 장비 라이선스를 먼저 설치한다. 저장소의 점검은 레지스트리와
벤더 절차로 Tornado2/K3D x64와 장비 라이선스를 회사 표준 루트
`C:\K3DAsyncEngine`에 먼저 설치한다. native는
`DLL\x64\Release\K3DAsyncEngine.dll`, Interop은
`Bin\x64\C#\Interop.K3DAsyncEngineLib.dll`이어야 한다. 저장소의 점검은 레지스트리와
파일을 읽을 뿐 COM을 활성화하지 않는다. 2단계 초기화는 이 검사를 항상 수행하고 `Valid`,
`ComActivated=false`가 아니면 로컬 설정을 완료하지 않는다. 다음 명령은 동일한 검사를 별도로
다시 확인할 때 사용한다.
`ComActivated=false`가 아니면 로컬 설정을 완료하지 않는다. 다음 명령은 동일한 검사를
별도로 다시 확인할 때 사용한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Inspect-K3DRegistration.ps1
```
Registry64, TypeLib/CLSID 양방향 매핑, `Apartment`, AMD64 PE, HKCU override 부재 검사가 모두
성공해야 한다. 점검 성공은 라이선스 성공을 대신하지 않으므로 벤더 방식으로 라이선스도 별도
확인한다. 첫 실행 설정은 이 검사를 통과한 native와 Interop 파일의 SHA-256을 최초 기준으로
고정하며, 이후 실제 파일이 기준값과 하나라도 다르면 중단한다. 수동 승인 모드에서는 두 파일이
각각 독립 승인된 SHA-256과 일치해야 한다.
Registry64 TypeLib, KAEngine, KAEventHandler가 모두 위 표준 native DLL을 가리키고,
CLSID/ProgID 양방향 매핑, `Apartment`, AMD64 PE, HKCU override 부재 검사가 모두 성공해야
한다. 다른 설치 루트나 fallback은 허용하지 않는다. 점검 성공은 라이선스 성공을 대신하지
않으므로 벤더 방식으로 라이선스도 별도 확인한다. 첫 실행 설정은 이 검사를 통과한 native와
Interop 파일의 SHA-256을 최초 기준으로 고정하며, 이후 실제 파일이 기준값과 하나라도 다르면
중단한다. 수동 승인 모드에서는 두 파일이 각각 독립 승인된 SHA-256과 일치해야 한다.
지속 pin 파일의 경로는 다음과 같다.
@@ -293,6 +299,7 @@ powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Initialize-ExistingDevelopmentPc.ps1 `
-LegacyRuntimeSourceRoot '<Cuts와 Res의 공통 부모>' `
-NoFolderPicker `
-SkipDatabaseProfile `
-ConfigureDevelopmentLive `
-PlayoutHost 127.0.0.1 `
-PlayoutPort 30001 `
@@ -302,17 +309,19 @@ powershell -NoProfile -ExecutionPolicy Bypass `
-ReplaceLiveConfig
```
runtime이나 DB 위치도 의도적으로 달라졌을 때만 각각 `-ReplaceRuntimeBinding`,
`-ReplaceDatabaseProfile`을 추가한다. 해시가 실제 등록 파일과 다르거나 K3D 등록 검사가
실패하면 기존 pin을 보존하고 중단한다.
runtime 위치도 의도적으로 달라졌을 때만 `-ReplaceRuntimeBinding`을 추가한다. 선택한
`Res\MmoneyCoder.ini`는 계속 직접 사용하므로 이 흐름에서 DB profile을 복사하거나
교체하지 않는다. 해시가 실제 등록 파일과 다르거나 K3D 등록 검사가 실패하면 기존 pin을
보존하고 중단한다.
## 6. 보호된 로컬 송출 설정
별도 수동 Development Live 초기화는 지속 K3D pin을 확인한 뒤 아래 송출 설정 두 파일도
마지막 기능 설정 단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug 빌드가 모두
성공하지 않으면 새 Live 승인 파일을 남기지 않는다. 자산 없는 PC의 3단계 bundle 흐름처럼
설정만 별도로 초기화해야 할 때는 저장소 루트에서 다음 스크립트를 실행한다. 이 수동 경로의
두 해시는 별도로 확인한 값이어야 한다. 지정 개발 PC의 endpoint는
2단계 자동 설정과 별도 수동 Development Live 초기화는 지속 K3D pin을 확인한 뒤 아래
송출 설정 두 파일도 마지막 기능 설정 단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저
무효화하며 runtime·DB 설정·Debug 빌드가 모두 성공하지 않으면 새 Live 승인 파일을 남기지
않는다. 자산 없는 PC의 3단계 bundle 흐름처럼 설정만 별도로 초기화해야 할 때는 저장소
루트에서 다음 스크립트를 실행한다. 이 수동 경로의 두 해시는 별도로 확인한 값이어야 한다.
지정 개발 PC의 endpoint는
`127.0.0.1:30001`로 고정한다. 기존 승인 프로필이 별도 출력 채널을 사용할 때만 검증된 숫자를
`-OutputChannel`로 추가하며, 기본 player를 쓰는 경우에는 생략한다.
@@ -344,7 +353,7 @@ powershell -NoProfile -ExecutionPolicy Bypass `
| `host` | 로컬 PGM이면 숫자형 loopback `127.0.0.1`. `localhost` 또는 원격 주소를 추정하지 않는다. |
| `port` | 지정 개발 환경의 고정 Network Server TCP port `30001` |
| `tcpMode` / `clientPort` | `1` / `0` |
| `sceneDirectory` | `null`. 검증된 로컬 runtime이 빌드 출력에 배치한 기본 `Cuts`용한다. |
| `sceneDirectory` | `null`. 앱이 운영자 설정에 저장된 정확한 외부 `Cuts` 경로용한다. |
| `outputChannel` | 승인된 개발 PGM 라우팅 값. 기존 승인 프로필이 기본 player를 쓰는 경우에만 `null` |
| `testSceneAllowlist` | 아래 active alias 45개만 허용 |
| `trustedLiveOutputEnabled` | `true` |

View File

@@ -11,6 +11,12 @@
이 문서는 개발 PC의 Visual Studio 2026에서 `MBN_STOCK_WEBVIEW.LegacyParityApp`
명확한 시작 대상으로 실행하고, 개발 Tornado2에 연결하는 방법을 설명한다.
솔루션 루트의 `.vsconfig`**WinUI 애플리케이션 개발**
(`Microsoft.VisualStudio.Workload.Universal`) 워크로드를 요구한다. Visual Studio가 누락
구성 요소 설치를 안내하면 먼저 설치를 완료하고 Visual Studio를 다시 시작한다.
`GetLatestMSVCVersion``...\VC\Tools\MSVC`를 찾지 못하는 오류는 이 WinUI 워크로드가
빠진 설치에서 발생하는 사전 조건 오류이며 앱 코드나 K3D 연결 오류가 아니다.
지정 개발 PC의 Debug와 Release 전체 앱은 모두 보호된 `Live`로만 기동한다. 개발 Live는
다음 조건이 동시에 맞을 때 기존 Live 게이트를 설정한다.
@@ -24,21 +30,22 @@
새 clone처럼 아직 검증된 runtime 연결이 없는 경우의 첫 F5는 예외적으로 소스 전용 설정
앱을 연다. 이 프로세스는 `--development-live`를 적용하지 않고 DB와 송출을 모두 차단한다.
화면에서 기존 코더의 `Cuts``Res` 폴더, 두 개만 선택한다. 설정 버튼은 폴더 안의 파일을
열거·파싱·복사하지 않고 두 경로와 로컬 MSBuild binding만 저장한다. PowerShell,
`dotnet build`, DB 복사, K3D 검사와 Live 승인 발급도 실행하지 않으므로 즉시 완료되어야
한다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 전체 앱 빌드가 시작된다.
화면에서 기존 코더의 `Cuts``Res` 폴더, 두 개만 선택한다. 설정 버튼은 선택한
`Res\MmoneyCoder.ini`의 DB 형식을 확인하고, 검증된 PowerShell 초기화기를 통해 두 경로,
로컬 MSBuild binding, `C:\K3DAsyncEngine` x64 등록/파일, 지속 K3D pin, Development Live
승인 및 Debug x64 빌드를 함께 검증한다. 이 작업은 시간이 걸릴 수 있다.
기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서 전체 앱 기동이 차단된다.
DB overlay, K3D pin과 Development Live 승인이 필요하면 아래 수동 초기화 절차를 별도로
완료한다. 자세한 절차는
설정 중에는 DB 연결, K3D COM 활성화, Tornado2/PGM 연결 또는 송출 명령을 실행하지 않는다.
설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 검증된 전체 앱이 시작된다. 기존 로컬
설정과 충돌하거나 K3D pin이 달라 자동 설정이 중단되면 대상을 검토한 뒤 아래 수동 교체
절차를 사용한다. 자세한 절차는
[개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를
따른다.
`MmoneyCoder.ini`의 DB 자격증명은 테스트 개발 서버용이라도 Git에 넣지 않는다. 첫 실행
경로 저장은 이 파일을 복사하거나 읽지 않는다. 빌드 출력, 선택 경로 설정과 MSIX에도
포함하지 않는다. 보호된 LocalAppData DB overlay와 Live 설정이 필요할 때만
`Initialize-ExistingDevelopmentPc.ps1`을 수동으로 사용한다.
설정은 파일을 복사하지 않고 형식만 검증한다. 빌드 출력, 선택 경로 설정과 MSIX에도
포함하지 않는다. 전체 앱은 저장된 `Res`의 정확한 `MmoneyCoder.ini`를 매 실행 직접 읽으며,
누락·손상된 파일을 LocalAppData나 환경 변수로 대체하지 않고 Live 기동을 차단한다.
## Visual Studio 시작 대상과 프로필
@@ -71,9 +78,9 @@ Studio의 시작 드롭다운에는 다음 프로필 하나만 표시된다.
### 지속 K3D pin
수동 Development Live 초기화는 K3D의 HKLM x64 등록, CLSID·ProgID·TypeLib, AMD64와 vendor 배치를 먼저
검증한다. 검사를 통과한 최초 native/Interop DLL의 SHA-256은 다음 사용자 전용 파일에
지속 저장한다.
첫 실행 자동 설정과 수동 Development Live 초기화는 모두 `C:\K3DAsyncEngine`의 HKLM x64
등록, CLSID·ProgID·TypeLib, AMD64와 회사 표준 vendor 배치를 먼저 검증한다. 검사를 통과한
최초 native/Interop DLL의 SHA-256은 다음 사용자 전용 파일에 지속 저장한다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json
@@ -123,9 +130,10 @@ Studio의 시작 드롭다운에는 다음 프로필 하나만 표시된다.
`authorization-unavailable` 결과로 처리하며 경로와 값은 로그에 표시하지 않는다.
수동 생성·교체 시에는 현재 설치 파일의 해시를 그 자리에서 계산했다는 이유만으로 승인 값으로
사용해서는 안 된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 일반
수동 Development Live 초기화에서는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤 같은 기준값으로 이 승인 파일을
발급한다. 파일 ACL은 현재 개발 사용자와 관리자만 수정할 수 있도록 유지한다.
사용해서는 안 된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 첫 실행
자동 설정과 일반 수동 Development Live 초기화는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤
같은 기준값으로 이 승인 파일을 발급한다. 파일 ACL은 현재 개발 사용자와 관리자만 수정할 수
있도록 유지한다.
## 기존 playout.local.json

View File

@@ -35,15 +35,16 @@ Web\...
소스 전용 앱은 메인 송출 화면 대신 `Cuts``Res` 두 폴더만 받는 네이티브 첫 실행 창을
표시한다. 이 프로세스는 DB를 초기화하지 않고 과거 로컬 송출 프로필, 운영자 송출·배경 경로,
프로세스 환경 override와 Development Live 실행 인수를 사용하지 않으며 안전한 DryRun을
강제한다. 설정이 성공하면 창을 닫고 Visual Studio에서 F5를 한 번 더 눌러 전체 자산
프로세스 환경 override와 Development Live 실행 인수를 사용하지 않으며 송출 엔진 자체를
만들지 않는다. 설정이 성공하면 창을 닫고 Visual Studio에서 F5를 한 번 더 눌러 전체 자산
빌드를 시작한다. 출력과 중간 패키징 파일은 이전 전체 빌드의 자산이나
`MmoneyCoder.ini`를 재사용하지 않도록 다음 별도 경로에 생성된다.
첫 실행 화면은 선택한 두 폴더의 고정 로컬 경로와 `Cuts`/`Res` 이름·공통 부모 확인한다.
폴더 내부 파일을 열거·파싱·복사하지 않고 PowerShell, 빌드, DB, K3D 또는 Live 설정도
실행하지 않는다. DB overlay와 Development Live 준비가 필요한 경우에만
`Initialize-ExistingDevelopmentPc.ps1`을 별도 수동 절차로 사용한다.
첫 실행 화면은 선택한 두 폴더의 고정 로컬 경로와 `Cuts`/`Res` 이름·공통 부모 확인하고
정확한 `Res\MmoneyCoder.ini`의 Oracle/MariaDB 형식을 검증한다. 이어서 검증된
`Initialize-ExistingDevelopmentPc.ps1`로 경로 binding, `C:\K3DAsyncEngine` x64 등록/파일,
지속 pin, Development Live 승인과 Debug x64 빌드를 확인한다. DB INI는 복사하지 않으며
DB 연결, K3D COM 활성화 또는 Tornado2/PGM 명령은 실행하지 않는다.
```text
src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\SourceOnly\...
@@ -87,7 +88,8 @@ Git에는 `Cuts`, DB 자격증명, 벤더 DLL, 라이선스, 인증서 또는
좌측 메뉴 하단의 `설정`에서는 네이티브 폴더 선택 창으로 디자인(`Cuts`) 폴더, 설정(`Res`)
폴더와 선택 사항인 운영 배경 폴더를 직접 지정할 수 있다. 선택한 고정 로컬 경로와 `Res`
폴더 자체만 확인한 뒤 다음 앱 전용 파일에 저장한다. 폴더 내용 전체는 검사하지 않는다.
폴더 자체만 확인한 뒤 다음 앱 전용 파일에 저장한다. 폴더 내용 전체는 검사하지 않지만,
Development Live는 다음 시작에서 선택한 `Res\MmoneyCoder.ini`를 별도로 엄격하게 검증한다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json
@@ -98,9 +100,9 @@ Git에는 `Cuts`, DB 자격증명, 벤더 DLL, 라이선스, 인증서 또는
저장된다. 절대 경로는 Web 콘텐츠에 전달하지 않는다.
이 화면은 자산 위치와 메뉴 시작 상태만 관리한다.
DB 자격증명의 내용은 표시하거나 직접 편집하지 않는다. Release/default `DryRun`, KTAP 호스트·
포트·채널, Test/Live 게이트, scene allowlist와 벤더 해시는 보호 설정으로 유지되며 폴더를 선택해도
완화되거나 변경되지 않는다.
DB 자격증명의 내용은 표시하거나 직접 편집하지 않는다. Debug/Release Live 라우팅, KTAP
호스트·포트·채널, Test/Live 게이트, scene allowlist와 벤더 해시는 보호 설정으로 유지되며
폴더를 선택해도 완화되거나 변경되지 않는다.
## Res와 DB 설정
@@ -109,22 +111,26 @@ CP949 레거시 형식으로 읽고, 파일의 section/row/order를 기존의
대조한 뒤 UI를 구성한다. INI가 임의의 scene/action을 새로 추가할 수는 없다.
`MmoneyCoder.ini`는 실제 자격증명을 포함하므로 빌드 입력, 출력, 개발 인수용 런타임
묶음과 MSIX Content에 넣지 않는다. 전체 자산 Debug 빌드도 깨끗한 개발 PC와 같은 다음
사용자 전용 로컬 overlay 또는 `Config\database.local.json`을 사용한다.
묶음과 MSIX Content에 넣지 않는다. 저장된 `resourceDirectory`가 있는 Development Live는
그 폴더의 정확한 `MmoneyCoder.ini`를 매 실행 직접 읽는다. 파일이 없거나 안전·형식 검증에
실패해도 다른 DB 설정으로 대체하지 않고 Live 엔진 생성과 자동 연결을 차단한다.
다음 사용자 전용 로컬 overlay와 `Config\database.local.json`은 저장된
`resourceDirectory`가 없는 기존 설치에서만 호환 fallback으로 사용한다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini
```
이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 폴더
저장은 원본 `MmoneyCoder.ini`를 읽거나 복사하지 않는다. 수동 Development Live 초기화에서
필요할 때만 위 사용자 전용 경로에 복사하고 현재 Windows 사용자 전용 ACL로 보호한다.
DB 자격증명은 Git, Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지 않는다.
이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 설정은
원본 `MmoneyCoder.ini`의 형식만 확인하고 복사하지 않는다. DB 자격증명은 Git,
Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지 않는다.
사용자 설정 메뉴에서 별도 `Res` 폴더를 선택하면 앱은 경로만 저장한다. 일반 Debug/DryRun은
다음 시작부터 그 폴더의 DB 설정을 사용할 수 있지만 형식 검사는 실제 로드 시 수행한다. 파일 내용과
자격증명은 Web 화면에 노출하지 않으며 화면에서 직접 편집할 수도 없다. 단, one-shot Gate A
검증 실행에서는 이 사용자 선택을 무시하고 검증 계획에 고정된 `database.local.json`만 사용한다.
사용자 설정 메뉴에서 별도 `Res` 폴더를 선택하면 앱은 경로만 저장하고 다음 시작부터 그
폴더의 DB 설정을 사용한다. 파일 내용과 자격증명은 Web 화면에 노출하지 않으며 화면에서 직접
편집할 수도 없다. 검증된 Development Live는 DB 환경 override를 무시한다. 단, one-shot
Gate A 검증 실행에서는 이 사용자 선택을 무시하고 검증 계획에 고정된
`database.local.json`만 사용한다.
`MmoneyCoder - 복사본.ini`, `.bak`, `.zip`, `afiedt.buf.txt`는 런타임 소비자가 없고 과거
자격증명 또는 SQL 흔적을 담을 수 있어 어떤 출력/패키지에도 넣지 않는다. 실제 자산과 설정은

View File

@@ -17,10 +17,15 @@
## 확인된 x64 COM 등록
현재 개발 장비에서 확인할 등록 기준은 다음과 같습니다.
회사 표준 K3D 설치 루트는 정확히 `C:\K3DAsyncEngine`입니다. 다른 드라이브나
같은 하위 구조를 가진 대체 루트는 허용하지 않습니다. 현재 개발 장비에서 확인할 등록
기준은 다음과 같습니다.
| 항목 | 기대값 |
|---|---|
| 설치 루트 | `C:\K3DAsyncEngine` |
| Native DLL | `C:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll` |
| C# Interop DLL | `C:\K3DAsyncEngine\Bin\x64\C#\Interop.K3DAsyncEngineLib.dll` |
| 레지스트리 뷰 | `HKLM\SOFTWARE\Classes``Registry64`; 관련 HKCU override 없음 |
| TypeLib GUID | `{2B7F2D64-3A8D-401C-BE73-5C0747BA342C}` |
| TypeLib 버전/대상 | `1.0` / `0\win64` |
@@ -28,9 +33,10 @@
| ProgID | `K3DAsyncEngine.KAEngine.1` |
| KAEventHandler CLSID / ProgID | `{39828C77-EFF0-4E59-979B-8673C028C718}` / `K3DAsyncEngine.KAEventHandler.1` |
| ThreadingModel | `Apartment` |
| DLL PE 대상 | TypeLib, KAEngine, KAEventHandler 모두 `AMD64` |
| 등록 경로 | TypeLib, KAEngine, KAEventHandler 모두 위의 정확한 Native DLL |
| DLL PE 대상 | Native DLL과 C# Interop DLL 모두 `AMD64` |
[Inspect-K3DRegistration.ps1](../scripts/Inspect-K3DRegistration.ps1)은 64비트 HKLM 등록에서 GUID, 버전과 `win64` TypeLib을 확인합니다. KAEngine과 KAEventHandler 각각의 CLSID→ProgID 및 ProgID→CLSID 양방향 매핑, `Apartment` 모델, `InprocServer32` 존재와 AMD64 PE 헤더 검사합니다. HKCU 64비트 `Software\Classes`에 같은 TypeLib GUID, CLSID 또는 ProgID override가 하나라도 있으면 fail-closed합니다. 레지스트리와 파일을 읽기만 하며 COM을 활성화하거나 등록을 변경하지 않습니다.
[Inspect-K3DRegistration.ps1](../scripts/Inspect-K3DRegistration.ps1)은 64비트 HKLM 등록에서 GUID, 버전과 `win64` TypeLib을 확인합니다. Registry64의 TypeLib, KAEngine 및 KAEventHandler가 모두 정확히 `C:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll`을 가리켜야 합니다. KAEngine과 KAEventHandler 각각의 CLSID→ProgID 및 ProgID→CLSID 양방향 매핑, `Apartment` 모델, `InprocServer32` 존재와 AMD64 PE 헤더 검사하고, 표준 위치의 `Interop.K3DAsyncEngineLib.dll`도 존재하는 AMD64 파일인지 확인합니다. HKCU 64비트 `Software\Classes`에 같은 TypeLib GUID, CLSID 또는 ProgID override가 하나라도 있으면 fail-closed합니다. 레지스트리와 파일을 읽기만 하며 COM을 활성화하거나 등록을 변경하지 않습니다.
`MBN_STOCK_WEBVIEW.PlayoutSmoke --probe`는 런타임과 같은 검사기를 통해 KAEngine과 KAEventHandler 등록을 함께 확인합니다. 이 probe도 COM 객체를 생성하지 않습니다.
@@ -39,17 +45,17 @@ powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Inspect-K3DRegistration.ps1
```
검사가 실패하면 x86 등록으로 대체하거나 DLL을 앱 폴더에 복사하지 않습니다. 벤더가 제공한 x64 설치 프로그램과 라이선스 절차로 장비 상태를 복구한 뒤 다시 검사합니다.
검사가 실패하면 다른 설치 루트나 x86 등록으로 대체하거나 DLL을 앱 폴더에 복사하지 않습니다. 벤더가 제공한 x64 설치 프로그램과 라이선스 절차로 `C:\K3DAsyncEngine`의 표준 설치 및 Registry64 등록 상태를 복구한 뒤 다시 검사합니다.
## 런타임 참조 방식
앱은 빌드 시점의 `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` 구현 안에 격리합니다.
앱은 빌드 시점의 `Interop.K3DAsyncEngineLib.dll`을 참조하거나 패키징하지 않습니다. 런타임에는 HKLM Registry64의 TypeLib·KAEngine·KAEventHandler 세 등록이 모두 정확히 `C:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll`을 가리키고 HKCU override가 없을 때만 `C:\K3DAsyncEngine\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>'
$approvedNative = 'C:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll'
$approvedInterop = 'C:\K3DAsyncEngine\Bin\x64\C#\Interop.K3DAsyncEngineLib.dll'
$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
@@ -177,7 +183,7 @@ COM 경로에는 `KTAPConnect`와 성공 후 최대 한 번의 `Disconnect`만
성공 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에 복사하지 않습니다.
x64 SDK의 네이티브 DLL과 정식 Interop은 위의 회사 표준 고정 경로·Registry64 일치·reparse ancestry·AMD64 PE·운영자별 SHA-256 핀을 모두 통과해야 합니다. 두 파일을 쓰기/교체할 수 없게 연 핸들은 COM 활성화와 사용을 포함한 프로세스 수명 동안 유지합니다. Interop 로드 후에는 assembly/TypeLib 버전, COM import GUID와 허용 메서드 서명도 확인합니다. vendor DLL은 Git 또는 MSIX에 복사하지 않습니다.
### 실제 PGM 컷 시퀀스 검증
@@ -198,7 +204,7 @@ Connect → Prepare(5001) → Play → 5초 관찰
| `5001.t2s` | `99CE3B689A42D8C42BEB09A86FA10C2D7C1AEF4F50D324D81276C1A1E4C4D8A7` |
| `5006.t2s` | `25CD0AE931F51E4E3B84CE3E6FD21A40DB85464F157A23CC3511D63B336D8757` |
재검증이 승인된 경우에는 임의의 `dotnet run` 대신 [Invoke-PgmCutsSequenceEvidence.ps1](../scripts/Invoke-PgmCutsSequenceEvidence.ps1)을 사용합니다. 먼저 x64 빌드 산출물과 두 컷의 변경이 없는지 독립적으로 검토하고, vendor/관리자가 승인한 네이티브·Interop 해시와 검토한 runner 산출물 해시를 인수로 전달합니다. `Approved*Sha256` 값은 실행 시점의 파일을 단순 계산해 곧바로 승인한 값으로 사용하지 않습니다. 실제 vendor 설치 경로와 컷 루트는 로컬 승인 기록에만 보관합니다.
재검증이 승인된 경우에는 임의의 `dotnet run` 대신 [Invoke-PgmCutsSequenceEvidence.ps1](../scripts/Invoke-PgmCutsSequenceEvidence.ps1)을 사용합니다. 먼저 x64 빌드 산출물과 두 컷의 변경이 없는지 독립적으로 검토하고, vendor/관리자가 승인한 네이티브·Interop 해시와 검토한 runner 산출물 해시를 인수로 전달합니다. `Approved*Sha256` 값은 실행 시점의 파일을 단순 계산해 곧바로 승인한 값으로 사용하지 않습니다. 두 K3D 파일이 회사 표준 고정 경로에 있고 Registry64와 일치하는지 확인하며 컷 루트는 로컬 승인 기록에만 보관합니다.
```powershell
$smoke = '<reviewed-x64-runner>\MBN_STOCK_WEBVIEW.PlayoutSmoke.dll'
@@ -390,7 +396,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 활성화가 막히면 Live 엔진을 `Faulted`/`Disconnected`로 유지하고 HRESULT와 등록 검사 결과만 보고합니다.
패키지 스모크에서는 벤더 x64 COM이 회사 표준 K3D 경로를 대상으로 Registry64에 정식 등록되어 있어야 합니다. MSIX에 벤더 DLL을 복사해 활성화 오류를 우회하지 않습니다. 패키지 컨텍스트에서 COM 활성화가 막히면 Live 엔진을 `Faulted`/`Disconnected`로 유지하고 HRESULT와 등록 검사 결과만 보고합니다.
## 장애 및 롤백
@@ -416,4 +422,4 @@ Round H cleanup은 승인된 TAKE OUT 1회와 두 scene unload, disconnect, 앱
- 운영 호스트, 채널, 창 제목 및 로컬 허용 목록이 든 설정
- 실제 출력 캡처나 비밀정보가 포함된 진단 로그
벤더 바이너리와 자산은 승인된 설치·배포 위치에서 관리하고, 앱 저장소에는 COM 중립 인터페이스와 안전한 설정 예시만 유지합니다.
벤더 K3D 바이너리는 회사 표준 `C:\K3DAsyncEngine` 설치 위치에서 관리하고, 자산은 승인된 배포 위치에서 관리합니다. 앱 저장소에는 COM 중립 인터페이스와 안전한 설정 예시만 유지합니다.

View File

@@ -45,6 +45,9 @@ $expectedResAssetCount = 34
$manifestFileName = 'LegacyRuntimeBundle.manifest.json'
$archiveFileName = 'LegacyRuntimeBundle.zip'
$archiveHashFileName = 'LegacyRuntimeBundle.zip.sha256'
$standardK3dRoot = 'C:\K3DAsyncEngine'
$standardK3dNativePath = Join-Path $standardK3dRoot 'DLL\x64\Release\K3DAsyncEngine.dll'
$standardK3dInteropPath = Join-Path $standardK3dRoot 'Bin\x64\C#\Interop.K3DAsyncEngineLib.dll'
function Get-NormalizedFullPath {
param([Parameter(Mandatory = $true)][string] $Path)
@@ -1132,7 +1135,10 @@ function Get-VerifiedK3DRegistration {
[string] $registration.TypeLibTarget -cne 'win64' -or
[string] $registration.TypeLibMachine -cne 'AMD64' -or
$registration.PowerShellProcessIs64Bit -ne $true -or
$registration.ComActivated -ne $false) {
$registration.ComActivated -ne $false -or
-not (Test-SamePath `
-Left ([string] $registration.TypeLibPath) `
-Right $standardK3dNativePath)) {
throw 'The read-only K3D x64 registration inspection result is invalid.'
}
@@ -1187,7 +1193,10 @@ function Get-VerifiedK3DRegistration {
[string] $expectedClasses[$name].ProgId -or
[string] $classReport.ServerMachine -cne 'AMD64' -or
[string] $classReport.ThreadingModel -cne 'Apartment' -or
$classReport.ReciprocalMapping -ne $true) {
$classReport.ReciprocalMapping -ne $true -or
-not (Test-SamePath `
-Left ([string] $classReport.ServerPath) `
-Right $standardK3dNativePath)) {
throw 'The read-only K3D x64 class registration shape is invalid.'
}
}
@@ -2026,32 +2035,24 @@ $registrationReports = @(& $registrationInspectorPath)
$registration = Get-VerifiedK3DRegistration -Reports $registrationReports
$registeredClasses = @($registration.Classes)
$nativePath = Get-NormalizedFullPath -Path ([string] $registration.TypeLibPath)
[void](Assert-RegularFile `
-Path $standardK3dNativePath `
-Description 'Standard K3D native DLL')
if (-not (Test-SamePath -Left $nativePath -Right $standardK3dNativePath)) {
throw 'The K3D Registry64 type library is not registered to the company-standard native DLL.'
}
[void](Assert-RegularFile -Path $nativePath -Description 'Registered K3D native DLL')
Assert-FixedLocalPath -Path $nativePath -Description 'Registered K3D native DLL'
foreach ($classReport in $registeredClasses) {
if (-not (Test-SamePath `
-Left ([string] $classReport.ServerPath) `
-Right $nativePath)) {
throw 'The K3D classes and type library do not resolve to one native DLL.'
throw 'The K3D Registry64 classes do not resolve to the company-standard native DLL.'
}
}
$releaseDirectory = [IO.Directory]::GetParent($nativePath)
$x64Directory = if ($null -ne $releaseDirectory) { $releaseDirectory.Parent } else { $null }
$dllDirectory = if ($null -ne $x64Directory) { $x64Directory.Parent } else { $null }
$vendorRoot = if ($null -ne $dllDirectory) { $dllDirectory.Parent } else { $null }
if ($null -eq $vendorRoot -or
-not ([IO.Path]::GetFileName($nativePath)).Equals(
'K3DAsyncEngine.dll',
[StringComparison]::OrdinalIgnoreCase) -or
-not $releaseDirectory.Name.Equals('Release', [StringComparison]::OrdinalIgnoreCase) -or
-not $x64Directory.Name.Equals('x64', [StringComparison]::OrdinalIgnoreCase) -or
-not $dllDirectory.Name.Equals('DLL', [StringComparison]::OrdinalIgnoreCase)) {
throw 'The registered K3D native DLL is outside the supported vendor layout.'
}
$interopPath = Get-NormalizedFullPath -Path (
Join-Path $vendorRoot.FullName 'Bin\x64\C#\Interop.K3DAsyncEngineLib.dll')
[void](Assert-RegularFile -Path $interopPath -Description 'Derived K3D interop assembly')
$interopPath = Get-NormalizedFullPath -Path $standardK3dInteropPath
[void](Assert-RegularFile -Path $interopPath -Description 'Standard K3D interop assembly')
Assert-FixedLocalPath -Path $interopPath -Description 'Derived K3D interop assembly'
$localAppData = [Environment]::GetFolderPath(

View File

@@ -10,6 +10,9 @@ $expectedTypeLibId = '{2B7F2D64-3A8D-401C-BE73-5C0747BA342C}'
$expectedTypeLibVersion = '1.0'
$expectedThreadingModel = 'Apartment'
$amd64Machine = 0x8664
$expectedVendorRoot = 'C:\K3DAsyncEngine'
$expectedNativePath = Join-Path $expectedVendorRoot 'DLL\x64\Release\K3DAsyncEngine.dll'
$expectedInteropPath = Join-Path $expectedVendorRoot 'Bin\x64\C#\Interop.K3DAsyncEngineLib.dll'
$expectedClasses = @(
[pscustomobject]@{
Name = 'KAEngine'
@@ -226,7 +229,10 @@ function Assert-ComClassRegistration {
[Microsoft.Win32.RegistryKey] $MachineRoot,
[Parameter(Mandatory = $true)]
[object] $Registration
[object] $Registration,
[Parameter(Mandatory = $true)]
[string] $ExpectedServerPath
)
$classesPrefix = 'SOFTWARE\Classes'
@@ -269,6 +275,10 @@ function Assert-ComClassRegistration {
$serverPath = Resolve-RegisteredFilePath `
-RegistryValue (Get-RegistryValue -BaseKey $MachineRoot -SubKey $inprocKey) `
-Description "$($Registration.Name) InprocServer32 path"
Assert-TextEqual `
-Actual $serverPath `
-Expected $ExpectedServerPath `
-Description "$($Registration.Name) registered native DLL path"
$threadingModel = Get-RegistryValue `
-BaseKey $MachineRoot `
-SubKey $inprocKey `
@@ -308,13 +318,22 @@ try {
$typeLibPath = Resolve-RegisteredFilePath `
-RegistryValue (Get-RegistryValue -BaseKey $machineRoot -SubKey $typeLibKey) `
-Description 'K3D x64 type library path'
Assert-TextEqual `
-Actual $typeLibPath `
-Expected $expectedNativePath `
-Description 'K3D x64 type library registered native DLL path'
Assert-Amd64File -Path $typeLibPath -Description 'K3D type library DLL'
$interopPath = Resolve-RegisteredFilePath `
-RegistryValue $expectedInteropPath `
-Description 'K3D x64 interop assembly path'
Assert-Amd64File -Path $interopPath -Description 'K3D interop assembly'
$classReports = @()
foreach ($registration in $expectedClasses) {
$classReports += Assert-ComClassRegistration `
-MachineRoot $machineRoot `
-Registration $registration
-Registration $registration `
-ExpectedServerPath $expectedNativePath
}
[pscustomobject][ordered]@{

View File

@@ -63,24 +63,37 @@ public sealed class DatabaseRuntime
}
/// <summary>
/// Creates the Development Live database runtime from the one protected
/// current-user LocalAppData legacy INI, or from the protected LocalAppData
/// JSON when that INI is absent. Unlike the ordinary compatibility path, this
/// never probes executable or operator-selected Res folders and ignores all
/// database environment overrides.
/// Creates the Development Live database runtime from the operator-selected
/// Res\MmoneyCoder.ini when a resource directory was selected. That exact INI
/// is authoritative and invalid or missing selected input never falls back.
/// Without an operator selection, the protected current-user LocalAppData INI,
/// or the protected LocalAppData JSON when that INI is absent, is used.
/// Development Live always ignores database environment overrides.
/// </summary>
public static DatabaseRuntime CreateDevelopmentLiveLocalOverride()
public static DatabaseRuntime CreateDevelopmentLiveLocalOverride(
string? operatorResourceDirectory = null)
{
var localApplicationData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
return CreateDevelopmentLiveLocalOverride(localApplicationData);
return CreateDevelopmentLiveLocalOverride(
localApplicationData,
operatorResourceDirectory);
}
internal static DatabaseRuntime CreateDevelopmentLiveLocalOverride(
string localApplicationData)
string localApplicationData,
string? operatorResourceDirectory)
{
try
{
if (operatorResourceDirectory is not null)
{
var selectedIniPath = ResolveSelectedDevelopmentLiveIniPath(
operatorResourceDirectory);
return Create(
new LegacyIniDatabaseOptionsLoader(_ => null).Load(selectedIniPath));
}
ArgumentException.ThrowIfNullOrWhiteSpace(localApplicationData);
var localRoot = Path.TrimEndingDirectorySeparator(
Path.GetFullPath(localApplicationData));
@@ -129,6 +142,37 @@ public sealed class DatabaseRuntime
}
}
private static string ResolveSelectedDevelopmentLiveIniPath(
string operatorResourceDirectory)
{
ArgumentException.ThrowIfNullOrWhiteSpace(operatorResourceDirectory);
var resourceDirectory = Path.TrimEndingDirectorySeparator(
Path.GetFullPath(operatorResourceDirectory));
var root = Path.GetPathRoot(resourceDirectory);
if (string.IsNullOrWhiteSpace(root) ||
root.StartsWith("\\\\", StringComparison.Ordinal) ||
string.Equals(
Path.TrimEndingDirectorySeparator(root),
resourceDirectory,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException();
}
var drive = new DriveInfo(root);
if (!drive.IsReady ||
drive.DriveType != DriveType.Fixed ||
!Directory.Exists(resourceDirectory))
{
throw new InvalidDataException();
}
EnsureNoReparsePointDirectoryChain(resourceDirectory, root);
var selectedIniPath = Path.Combine(resourceDirectory, "MmoneyCoder.ini");
EnsureOrdinaryFile(selectedIniPath);
return selectedIniPath;
}
private static string? ResolveLegacyOverridePath()
{
var localAppData = Environment.GetFolderPath(

View File

@@ -23,7 +23,7 @@
<TextBlock
FontSize="15"
Foreground="#FFBFD2E6"
Text="기존 코더의 Cuts 폴더와 Res 폴더를 선택하면 해당 경로만 저장합니다."
Text="기존 코더의 Cuts 폴더와 Res 폴더를 선택하면 이후 실행에서도 그 경로를 그대로 사용합니다."
TextWrapping="Wrap" />
</StackPanel>
@@ -66,7 +66,7 @@
Text="2. 설정/INI 폴더 (Res)" />
<TextBlock
Foreground="#FF8FAAC5"
Text="폴더 안의 파일 전체를 검사하거나 복사하지 않고 선택한 경로를 그대로 사용합니다."
Text="선택한 폴더의 MmoneyCoder.ini를 DB 설정으로 검증하고 계속 직접 사용합니다. 자격증명 파일은 복사하지 않습니다."
TextWrapping="Wrap" />
<TextBlock
x:Name="ResourcePathText"
@@ -115,7 +115,7 @@
<TextBlock
Foreground="#FF8FAAC5"
FontSize="13"
Text="설정 시작은 경로만 저장합니다. 빌드, DB, K3D 또는 Tornado2 작업은 실행하지 않으며 완료 후 Visual Studio에서 F5를 한 번 더 누르면 앱을 빌드해 시작합니다."
Text="설정 시작은 선택 경로, C:\K3DAsyncEngine의 x64 등록/파일, 보호된 Live 설정과 Debug x64 빌드를 검증합니다. 이 과정에서는 DB에 연결하거나 Tornado2/PGM 송출 명령을 보내지 않습니다."
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>

View File

@@ -1,6 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Xml;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using Microsoft.UI.Windowing;
using Windows.Graphics;
@@ -12,8 +12,8 @@ namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
public sealed partial class FirstRunSetupWindow : Window
{
private const string RepositoryRootMetadataKey = "SourceRepositoryRoot";
private const string LocalBuildPropsFileName =
"Directory.Build.local.props";
private const string SetupScriptRelativePath =
@"scripts\Initialize-ExistingDevelopmentPc.ps1";
private string? _cutsDirectory;
private string? _resourceDirectory;
@@ -82,7 +82,7 @@ public sealed partial class FirstRunSetupWindow : Window
RefreshReadyState();
}
private void OnInstallClicked(object sender, RoutedEventArgs e)
private async void OnInstallClicked(object sender, RoutedEventArgs e)
{
if (_setupRunning)
{
@@ -112,14 +112,14 @@ public sealed partial class FirstRunSetupWindow : Window
}
SetSetupRunning(true);
SetupStatusBar.Title = "경로 저장 중";
SetupStatusBar.Title = "개발 PC 설정 중";
SetupStatusBar.Message =
"선택한 Cuts/Res 폴더 경로를 저장하고 있습니다.";
"DB 설정, 표준 K3D 설치, Live 승인과 Debug x64 빌드를 검증하고 있습니다.";
SetupStatusBar.Severity = InfoBarSeverity.Informational;
try
{
SaveSelectedRuntimePaths(
await RunVerifiedSetupAsync(
repositoryRoot,
runtimeRoot!,
_cutsDirectory!,
@@ -127,7 +127,7 @@ public sealed partial class FirstRunSetupWindow : Window
SetupStatusBar.Title = "설정 완료";
SetupStatusBar.Message =
"폴더 경로를 저장했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
"Cuts/Res, DB, K3D 및 Live 설정을 검증했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
SetupStatusBar.Severity = InfoBarSeverity.Success;
InstallButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Visible;
@@ -135,7 +135,7 @@ public sealed partial class FirstRunSetupWindow : Window
catch
{
ShowError(
"폴더 경로를 저장하지 못했습니다. 폴더와 Git 소스 위치를 확인해 주세요.");
"개발 PC 자동 설정에 실패했습니다. Res\\MmoneyCoder.ini, C:\\K3DAsyncEngine x64 등록, Tornado2의 127.0.0.1:30001 설정을 확인해 주세요.");
}
finally
{
@@ -211,7 +211,7 @@ public sealed partial class FirstRunSetupWindow : Window
InstallButton.IsEnabled = true;
SetupStatusBar.Title = "준비 완료";
SetupStatusBar.Message =
"설정 시작을 누르면 선택한 Cuts/Res 경로를 이 PC에 적용합니다.";
"설정 시작을 누르면 Cuts/Res, DB, 표준 K3D 및 Development Live를 검증합니다.";
SetupStatusBar.Severity = InfoBarSeverity.Success;
}
@@ -267,6 +267,18 @@ public sealed partial class FirstRunSetupWindow : Window
return false;
}
try
{
_ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
Path.Combine(resource, "MmoneyCoder.ini"));
}
catch (DatabaseConfigurationException)
{
warningMessage =
"선택한 Res 폴더의 MmoneyCoder.ini에 유효한 Oracle/MariaDB 설정이 모두 필요합니다.";
return false;
}
runtimeRoot = cutsParent;
return true;
}
@@ -301,119 +313,141 @@ public sealed partial class FirstRunSetupWindow : Window
return repositoryRoot;
}
private static void SaveSelectedRuntimePaths(
private static async Task RunVerifiedSetupAsync(
string repositoryRoot,
string runtimeRoot,
string cutsDirectory,
string resourceDirectory)
{
var store = new LegacyOperatorSettingsStore();
var loaded = store.Load();
var current = loaded.Failure == LegacyOperatorSettingsStoreFailure.None
? loaded.Settings
: LegacyOperatorSettings.Default;
var saved = store.Save(current with
var powershellPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.System),
"WindowsPowerShell",
"v1.0",
"powershell.exe");
var setupScriptPath = Path.Combine(
repositoryRoot,
SetupScriptRelativePath);
EnsureOrdinaryFile(
powershellPath,
"Windows PowerShell을 찾을 수 없습니다.");
EnsureOrdinaryFile(
setupScriptPath,
"검증된 개발 PC 초기화 스크립트를 찾을 수 없습니다.");
var startInfo = new ProcessStartInfo
{
SceneDirectory = cutsDirectory,
ResourceDirectory = resourceDirectory
});
if (!saved.Succeeded)
FileName = powershellPath,
WorkingDirectory = repositoryRoot,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
AddArgument("-NoLogo");
AddArgument("-NoProfile");
AddArgument("-NonInteractive");
AddArgument("-ExecutionPolicy");
AddArgument("Bypass");
AddArgument("-File");
AddArgument(setupScriptPath);
AddArgument("-LegacyRuntimeSourceRoot");
AddArgument(runtimeRoot);
AddArgument("-NoFolderPicker");
AddArgument("-SkipDatabaseProfile");
AddArgument("-ConfigureDevelopmentLive");
AddArgument("-PlayoutHost");
AddArgument("127.0.0.1");
AddArgument("-PlayoutPort");
AddArgument("30001");
AddArgument("-PinRegisteredK3D");
using var process = new Process
{
StartInfo = startInfo
};
if (!process.Start())
{
throw new InvalidOperationException(
"The selected runtime folders could not be saved.");
"The verified development-PC initializer could not be started.");
}
SaveRuntimeBinding(repositoryRoot, runtimeRoot);
var standardOutput = DrainOutputAsync(process.StandardOutput);
var standardError = DrainOutputAsync(process.StandardError);
await process.WaitForExitAsync();
await Task.WhenAll(standardOutput, standardError);
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"The verified development-PC initializer failed with exit code {process.ExitCode}.");
}
var loaded = new LegacyOperatorSettingsStore().Load();
if (loaded.Failure != LegacyOperatorSettingsStoreFailure.None ||
!string.Equals(
loaded.Settings.SceneDirectory,
cutsDirectory,
StringComparison.OrdinalIgnoreCase) ||
!string.Equals(
loaded.Settings.ResourceDirectory,
resourceDirectory,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"The verified initializer did not retain the selected runtime folders.");
}
EnsureOrdinaryFile(
Path.Combine(repositoryRoot, "Directory.Build.local.props"),
"The verified initializer did not retain the local build binding.");
var localApplicationData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
var configurationRoot = Path.Combine(
localApplicationData,
"MBN_STOCK_WEBVIEW",
"Config");
foreach (var fileName in new[]
{
"k3d-pins.local.json",
"playout.local.json",
"playout.development-live.local.json"
})
{
EnsureOrdinaryFile(
Path.Combine(configurationRoot, fileName),
"The verified initializer did not retain every protected Live setting.");
}
void AddArgument(string value) => startInfo.ArgumentList.Add(value);
}
private static void SaveRuntimeBinding(
string repositoryRoot,
string runtimeRoot)
private static async Task DrainOutputAsync(StreamReader reader)
{
var destination = Path.Combine(
repositoryRoot,
LocalBuildPropsFileName);
if (Directory.Exists(destination))
var buffer = new char[4096];
while (await reader.ReadAsync(buffer.AsMemory()) > 0)
{
throw new InvalidOperationException(
"The local runtime binding path is not a file.");
}
}
if (File.Exists(destination))
{
var attributes = File.GetAttributes(destination);
if ((attributes & (FileAttributes.Directory |
FileAttributes.ReparsePoint |
FileAttributes.Device)) != 0)
{
throw new InvalidOperationException(
"The local runtime binding file is unsafe.");
}
}
var bytes = CreateRuntimeBinding(runtimeRoot);
var temporary = Path.Combine(
repositoryRoot,
$".{LocalBuildPropsFileName}.{Guid.NewGuid():N}.tmp");
private static void EnsureOrdinaryFile(string path, string message)
{
try
{
using (var stream = new FileStream(
temporary,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
FileOptions.WriteThrough))
var attributes = File.GetAttributes(Path.GetFullPath(path));
if ((attributes & (FileAttributes.Directory |
FileAttributes.ReparsePoint |
FileAttributes.Device)) == 0)
{
stream.Write(bytes);
stream.Flush(flushToDisk: true);
}
File.Move(temporary, destination, overwrite: true);
temporary = string.Empty;
if (!File.ReadAllBytes(destination).AsSpan().SequenceEqual(bytes))
{
throw new IOException(
"The local runtime binding failed verification.");
return;
}
}
finally
catch (Exception exception) when (
exception is ArgumentException or IOException or
UnauthorizedAccessException or NotSupportedException or
System.Security.SecurityException)
{
if (!string.IsNullOrEmpty(temporary) &&
File.Exists(temporary))
{
File.Delete(temporary);
}
}
}
private static byte[] CreateRuntimeBinding(string runtimeRoot)
{
using var stream = new MemoryStream(capacity: 512);
var settings = new XmlWriterSettings
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
Indent = true,
NewLineChars = Environment.NewLine,
NewLineHandling = NewLineHandling.Replace
};
using (var writer = XmlWriter.Create(stream, settings))
{
writer.WriteStartDocument();
writer.WriteStartElement("Project");
writer.WriteStartElement("PropertyGroup");
writer.WriteElementString(
"LegacyRuntimeSourceRoot",
runtimeRoot);
writer.WriteElementString(
"LegacyRuntimeAssetsMode",
"Required");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
return stream.ToArray();
throw new InvalidOperationException(message);
}
private void SetSetupRunning(bool running)

View File

@@ -185,8 +185,8 @@
</Content>
<!--
The active legacy DB INI contains credentials and is never a build input or
MSIX Content. Local F5 and packaged development use the current user's
LocalAppData overlay. Historical copies, backups, archives and
MSIX Content. Development Live reads the exact selected external
Res\MmoneyCoder.ini at runtime. Historical copies, backups, archives and
afiedt.buf.txt are not included at all.
-->
<Manifest Include="$(ApplicationManifest)" />
@@ -202,7 +202,7 @@
Text="Legacy runtime assets were not found. Building the safe first-run setup app. Start Debug x64 and select the existing Cuts and Res folders." />
<Message Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'"
Importance="high"
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="External legacy runtime paths are enabled. Cuts and the credential-bearing Res\MmoneyCoder.ini remain in the selected external folders." />
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyRuntimeSourceRoot)')"
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." />
@@ -242,8 +242,8 @@
</Target>
<!--
Database credentials are never build inputs. Full Debug, publish and MSIX
builds use the same user-only LocalAppData overlay as a clean handoff PC.
Database credentials are never build inputs. Development Live resolves the
selected external Res\MmoneyCoder.ini only at runtime.
-->
<Target Name="ScrubLegacyDatabaseIniFromBuildOutput"
BeforeTargets="PrepareForBuild">

View File

@@ -139,16 +139,17 @@ public sealed partial class MainWindow : Window
// A Gate A plan pins database.local.json by path, size and SHA-256.
// Never inspect either packaged or operator-selected legacy INI files
// during that one-shot validation process.
// during that one-shot validation process. Ordinary Development Live
// uses the selected Res\MmoneyCoder.ini as its exact DB authority.
var selectedLegacyDatabaseIni =
_isDevelopmentLiveLaunch ||
selectedResourceDirectory is null
? null
: Path.Combine(selectedResourceDirectory, "MmoneyCoder.ini");
_databaseRuntime = _playoutLaunchAuthorization.IsGateA
? DatabaseRuntime.CreateDefault()
: _isDevelopmentLiveLaunch
? DatabaseRuntime.CreateDevelopmentLiveLocalOverride()
? DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
selectedResourceDirectory)
: DatabaseRuntime.CreateLegacyExecutableDefault(
AppContext.BaseDirectory,
legacyOverridePath: selectedLegacyDatabaseIni,
@@ -208,11 +209,21 @@ public sealed partial class MainWindow : Window
operatorCatalogSchemaValidationService =
new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor);
}
catch
catch (Exception exception)
{
var developmentLiveDatabaseConfigurationFailed =
_isDevelopmentLiveLaunch &&
exception is DatabaseConfigurationException;
if (developmentLiveDatabaseConfigurationFailed)
{
_developmentLiveStartupBlocked = true;
}
_databaseRuntime = null;
fixedPagePlanProvider = null;
initializationError = _developmentLiveStartupBlocked
initializationError = developmentLiveDatabaseConfigurationFailed
? "Development Live 데이터베이스 설정을 확인할 수 없습니다."
: _developmentLiveStartupBlocked
? App.DevelopmentLiveStartupFailureMessage ??
"Live playout startup validation failed."
: IsSourceOnlyBuild

View File

@@ -133,37 +133,27 @@ internal static class InstalledK3dInteropMetadata
var typeLibraryPath = NormalizeRegisteredPath(snapshot.Win64TypeLibraryPath);
var enginePath = NormalizeRegisteredPath(snapshot.Engine.InprocServerPath);
var eventHandlerPath = NormalizeRegisteredPath(snapshot.EventHandler.InprocServerPath);
if (!string.Equals(typeLibraryPath, enginePath, StringComparison.OrdinalIgnoreCase) ||
!string.Equals(typeLibraryPath, eventHandlerPath, StringComparison.OrdinalIgnoreCase))
{
throw new InstalledK3dInteropMetadataUnavailableException();
}
var nativeFile = new FileInfo(typeLibraryPath);
var releaseDirectory = nativeFile.Directory;
var x64Directory = releaseDirectory?.Parent;
var dllDirectory = x64Directory?.Parent;
var vendorRoot = dllDirectory?.Parent;
var expectedNativePath = Path.GetFullPath(
K3dComConstants.StandardNativeBinaryPath);
if (!string.Equals(
nativeFile.Name,
"K3DAsyncEngine.dll",
typeLibraryPath,
expectedNativePath,
StringComparison.OrdinalIgnoreCase) ||
!string.Equals(releaseDirectory?.Name, "Release", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(x64Directory?.Name, "x64", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(dllDirectory?.Name, "DLL", StringComparison.OrdinalIgnoreCase) ||
vendorRoot is null)
!string.Equals(
enginePath,
expectedNativePath,
StringComparison.OrdinalIgnoreCase) ||
!string.Equals(
eventHandlerPath,
expectedNativePath,
StringComparison.OrdinalIgnoreCase))
{
throw new InstalledK3dInteropMetadataUnavailableException();
}
var interopPath = Path.GetFullPath(
Path.Combine(
vendorRoot.FullName,
"Bin",
"x64",
"C#",
"Interop.K3DAsyncEngineLib.dll"));
return new InstalledK3dInteropPaths(typeLibraryPath, interopPath);
return new InstalledK3dInteropPaths(
expectedNativePath,
Path.GetFullPath(K3dComConstants.StandardInteropAssemblyPath));
}
internal static byte[] ParseApprovedSha256(string? value)

View File

@@ -2,6 +2,12 @@ namespace MBN_STOCK_WEBVIEW.Playout.Interop;
public static class K3dComConstants
{
public const string StandardInstallationRoot = @"C:\K3DAsyncEngine";
public const string StandardNativeBinaryPath =
@"C:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll";
public const string StandardInteropAssemblyPath =
@"C:\K3DAsyncEngine\Bin\x64\C#\Interop.K3DAsyncEngineLib.dll";
public const string TypeLibraryId = "{2B7F2D64-3A8D-401C-BE73-5C0747BA342C}";
public const string TypeLibraryVersion = "1.0";

View File

@@ -24,7 +24,10 @@ public enum K3dRegistrationIssue
EventHandlerProgIdClassIdMismatch = 1 << 14,
EventHandlerServerIsNotAmd64 = 1 << 15,
CurrentUserOverridePresent = 1 << 16,
RegistryReadFailed = 1 << 17
RegistryReadFailed = 1 << 17,
TypeLibraryPathMismatch = 1 << 18,
EngineServerPathMismatch = 1 << 19,
EventHandlerServerPathMismatch = 1 << 20
}
public sealed record K3dRegistrationReport(
@@ -122,6 +125,10 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
{
issues |= K3dRegistrationIssue.TypeLibraryMissing;
}
else if (!IsStandardNativePath(snapshot.Win64TypeLibraryPath))
{
issues |= K3dRegistrationIssue.TypeLibraryPathMismatch;
}
if (!typeLibraryBinary.Exists)
{
@@ -140,6 +147,7 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
K3dRegistrationIssue.EngineProgIdMismatch,
K3dRegistrationIssue.EngineProgIdClassIdMismatch,
K3dRegistrationIssue.EngineServerIsNotAmd64,
K3dRegistrationIssue.EngineServerPathMismatch,
K3dRegistrationIssue.EngineThreadingModelMismatch,
K3dRegistrationIssue.EngineTypeLibraryMismatch,
ref issues);
@@ -152,6 +160,7 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
K3dRegistrationIssue.EventHandlerProgIdMismatch,
K3dRegistrationIssue.EventHandlerProgIdClassIdMismatch,
K3dRegistrationIssue.EventHandlerServerIsNotAmd64,
K3dRegistrationIssue.EventHandlerServerPathMismatch,
K3dRegistrationIssue.EventHandlerThreadingModelMismatch,
K3dRegistrationIssue.EventHandlerTypeLibraryMismatch,
ref issues);
@@ -181,6 +190,7 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
K3dRegistrationIssue classProgIdIssue,
K3dRegistrationIssue progIdClassIdIssue,
K3dRegistrationIssue architectureIssue,
K3dRegistrationIssue pathIssue,
K3dRegistrationIssue threadingIssue,
K3dRegistrationIssue typeLibraryIssue,
ref K3dRegistrationIssue issues)
@@ -213,6 +223,11 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
issues |= architectureIssue;
}
if (!IsStandardNativePath(registration.InprocServerPath))
{
issues |= pathIssue;
}
if (!string.Equals(
registration.ThreadingModel,
K3dComConstants.ApartmentThreadingModel,
@@ -231,6 +246,7 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
classProgIdIssue |
progIdClassIdIssue |
architectureIssue |
pathIssue |
threadingIssue |
typeLibraryIssue;
return (issues & classIssues) == 0;
@@ -239,6 +255,27 @@ public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
private static bool MatchesGuid(string? value, Guid expected) =>
Guid.TryParse(value, out var actual) && actual == expected;
private static bool IsStandardNativePath(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
try
{
return string.Equals(
Path.GetFullPath(value),
Path.GetFullPath(K3dComConstants.StandardNativeBinaryPath),
StringComparison.OrdinalIgnoreCase);
}
catch (Exception exception) when (
exception is ArgumentException or NotSupportedException or PathTooLongException)
{
return false;
}
}
private static string SafeMessage(K3dRegistrationIssue issues) => issues switch
{
K3dRegistrationIssue.None => "x64 K3D 송출 구성 요소가 준비되었습니다.",

View File

@@ -179,7 +179,7 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
}
[Fact]
public void Runtime_DevelopmentLiveUsesOnlyExactLocalAppDataIni()
public void Runtime_DevelopmentLiveUsesProtectedLocalAppDataIniWhenNoOperatorResIsSelected()
{
using var localApplicationData = TemporaryDirectory.Create();
var resourceDirectory = Path.Combine(
@@ -200,7 +200,8 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
environmentName,
"stale-environment.invalid");
runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path);
localApplicationData.Path,
operatorResourceDirectory: null);
}
finally
{
@@ -213,6 +214,170 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host);
}
[Fact]
public void Runtime_DevelopmentLiveSelectedResIniIsAuthoritativeAndIgnoresEnvironment()
{
using var localApplicationData = TemporaryDirectory.Create();
var protectedResourceDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Res");
Directory.CreateDirectory(protectedResourceDirectory);
File.WriteAllBytes(
Path.Combine(protectedResourceDirectory, "MmoneyCoder.ini"),
TemporaryLegacyIni.ValidUtf8Bytes());
using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes(
"""
[Oracle]
ConnectionName=selected.oracle.invalid:1523/SELECTED
ID=selected-oracle-user
Pass=selected-oracle-password
[Maria]
ConnectionName=selected.maria.invalid:3308/selected_stock
ID=selected-maria-user
Pass=selected-maria-password
"""));
var environmentOverrides = new Dictionary<string, string>
{
["MBN_STOCK_ORACLE_HOST"] = "stale-environment.invalid",
["MBN_STOCK_ORACLE_PORT"] = "not-an-integer",
["MBN_STOCK_MARIADB_TLS_MODE"] = "Required"
};
var originalEnvironment = environmentOverrides.Keys.ToDictionary(
static name => name,
Environment.GetEnvironmentVariable,
StringComparer.Ordinal);
DatabaseRuntime runtime;
try
{
foreach (var pair in environmentOverrides)
{
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
}
runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path,
selected.Directory);
}
finally
{
foreach (var pair in originalEnvironment)
{
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
}
}
Assert.Equal("selected.oracle.invalid", runtime.Options.Oracle.Host);
Assert.Equal(1523, runtime.Options.Oracle.Port);
Assert.Equal("SELECTED", runtime.Options.Oracle.Sid);
Assert.Equal("selected.maria.invalid", runtime.Options.MariaDb.Host);
Assert.Equal(3308, runtime.Options.MariaDb.Port);
Assert.Equal("selected_stock", runtime.Options.MariaDb.Database);
Assert.Equal(MariaDbTlsMode.Disabled, runtime.Options.MariaDb.TlsMode);
}
[Fact]
public void Runtime_DevelopmentLiveReadsSelectedIniOnEveryCreation()
{
using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes(
"""
[Oracle]
ConnectionName=first.oracle.invalid:1521/FIRST
ID=oracle-user
Pass=oracle-password
[Maria]
ConnectionName=first.maria.invalid:3306/first_stock
ID=maria-user
Pass=maria-password
"""));
var first = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(selected.Directory);
File.WriteAllBytes(selected.Path, Encoding.UTF8.GetBytes(
"""
[Oracle]
ConnectionName=second.oracle.invalid:1521/SECOND
ID=oracle-user
Pass=oracle-password
[Maria]
ConnectionName=second.maria.invalid:3306/second_stock
ID=maria-user
Pass=maria-password
"""));
var second = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(selected.Directory);
Assert.Equal("first.oracle.invalid", first.Options.Oracle.Host);
Assert.Equal("first.maria.invalid", first.Options.MariaDb.Host);
Assert.Equal("second.oracle.invalid", second.Options.Oracle.Host);
Assert.Equal("second.maria.invalid", second.Options.MariaDb.Host);
}
[Fact]
public void Runtime_DevelopmentLiveSelectedResDoesNotDependOnLocalAppDataRoot()
{
using var selected = TemporaryLegacyIni.CreateValidUtf8();
var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData: string.Empty,
operatorResourceDirectory: selected.Directory);
Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host);
Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host);
}
[Fact]
public void Runtime_DevelopmentLiveSelectedResDoesNotFallBackWhenExactIniIsMissing()
{
using var localApplicationData = TemporaryDirectory.Create();
var protectedResourceDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Res");
Directory.CreateDirectory(protectedResourceDirectory);
File.WriteAllBytes(
Path.Combine(protectedResourceDirectory, "MmoneyCoder.ini"),
TemporaryLegacyIni.ValidUtf8Bytes());
using var selectedResourceDirectory = TemporaryDirectory.Create();
File.WriteAllBytes(
Path.Combine(selectedResourceDirectory.Path, "MmoneyCoder.ini.bak"),
TemporaryLegacyIni.ValidUtf8Bytes());
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path,
selectedResourceDirectory.Path));
}
[Fact]
public void Runtime_DevelopmentLiveSelectedResDoesNotFallBackWhenIniIsInvalid()
{
const string secret = "selected-secret-sentinel";
using var localApplicationData = TemporaryDirectory.Create();
var protectedResourceDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Res");
Directory.CreateDirectory(protectedResourceDirectory);
File.WriteAllBytes(
Path.Combine(protectedResourceDirectory, "MmoneyCoder.ini"),
TemporaryLegacyIni.ValidUtf8Bytes());
using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes(
"""
[Oracle]
ConnectionName=invalid
ID=oracle-user
Pass=selected-secret-sentinel
"""));
var exception = Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path,
selected.Directory));
Assert.DoesNotContain(secret, exception.ToString(), StringComparison.Ordinal);
Assert.DoesNotContain(selected.Path, exception.ToString(), StringComparison.Ordinal);
}
[Fact]
public void Runtime_DevelopmentLiveFailsClosedWhenBothProtectedProfilesAreMissing()
{
@@ -224,7 +389,8 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path));
localApplicationData.Path,
operatorResourceDirectory: null));
}
[Fact]
@@ -260,7 +426,8 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
""");
var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path);
localApplicationData.Path,
operatorResourceDirectory: null);
Assert.Equal("json.oracle.invalid", runtime.Options.Oracle.Host);
Assert.Equal("json.maria.invalid", runtime.Options.MariaDb.Host);
@@ -280,7 +447,8 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path));
localApplicationData.Path,
operatorResourceDirectory: null));
}
[Fact]
@@ -296,7 +464,8 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path));
localApplicationData.Path,
operatorResourceDirectory: null));
}
[Fact]

View File

@@ -117,7 +117,7 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
}
[Fact]
public void AppliedDevelopmentLiveUsesSelectedRuntimePathsAndProtectedDatabaseInputs()
public void AppliedDevelopmentLiveUsesSelectedRuntimePathsAndSelectedResDatabaseAuthority()
{
var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs"));
var playout = File.ReadAllText(Path.Combine(
@@ -142,14 +142,15 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
window,
StringComparison.Ordinal);
Assert.Contains(
"_isDevelopmentLiveLaunch ||\n" +
"var selectedLegacyDatabaseIni =\n" +
" selectedResourceDirectory is null\n" +
" ? null",
window.ReplaceLineEndings("\n"),
StringComparison.Ordinal);
Assert.Contains(
"DatabaseRuntime.CreateDevelopmentLiveLocalOverride()",
window,
"? DatabaseRuntime.CreateDevelopmentLiveLocalOverride(\n" +
" selectedResourceDirectory)",
window.ReplaceLineEndings("\n"),
StringComparison.Ordinal);
Assert.Contains(
"? PlayoutOptionsLoader.LoadDevelopmentLive(",
@@ -168,7 +169,7 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
}
[Fact]
public void DatabaseFailureDoesNotDowngradeOrBlockValidatedLiveEngine()
public void InvalidDevelopmentLiveDatabaseConfigurationBlocksEngineAndAutomaticConnect()
{
var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs"))
.ReplaceLineEndings("\n");
@@ -177,44 +178,50 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
"MainWindow.Playout.cs"))
.ReplaceLineEndings("\n");
var runtimeReset = window.IndexOf(
"_databaseRuntime = null;",
var databaseFailureCondition = window.IndexOf(
"_isDevelopmentLiveLaunch &&\n" +
" exception is DatabaseConfigurationException;",
StringComparison.Ordinal);
var pagePlanReset = window.IndexOf(
"fixedPagePlanProvider = null;",
runtimeReset,
var startupBlock = window.IndexOf(
"_developmentLiveStartupBlocked = true;",
databaseFailureCondition,
StringComparison.Ordinal);
var databaseFailureEnd = window.IndexOf(
"_industryWorkflow = new LegacyIndustrySelectionWorkflow(",
pagePlanReset,
var safeMessage = window.IndexOf(
"\"Development Live 데이터베이스 설정을 확인할 수 없습니다.\"",
startupBlock,
StringComparison.Ordinal);
var playoutInitialization = window.IndexOf(
"InitializePlayoutRuntime();",
safeMessage,
StringComparison.Ordinal);
var automaticConnectGuard = window.IndexOf(
"if (!_developmentLiveStartupBlocked)",
playoutInitialization,
StringComparison.Ordinal);
var automaticConnect = window.IndexOf(
"await ConnectPlayoutAsync(_lifetimeCancellation.Token);",
automaticConnectGuard,
StringComparison.Ordinal);
var earlyBlock = playout.IndexOf(
"if (_developmentLiveStartupBlocked)",
StringComparison.Ordinal);
var earlyReturn = playout.IndexOf(
"return;",
earlyBlock,
StringComparison.Ordinal);
var createEngine = playout.IndexOf(
"_playoutEngine = PlayoutEngineFactory.Create(",
StringComparison.Ordinal);
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(
"_isDevelopmentLiveLaunch\n" +
" ? PlayoutOptionsLoader.LoadDevelopmentLive(",
playout,
StringComparison.Ordinal);
Assert.Contains(
"if (_playoutOptions.Mode != PlayoutMode.Live)",
playout,
StringComparison.Ordinal);
Assert.Contains(
"_playoutEngine = PlayoutEngineFactory.Create(",
playout,
StringComparison.Ordinal);
Assert.True(databaseFailureCondition >= 0);
Assert.True(startupBlock > databaseFailureCondition);
Assert.True(safeMessage > startupBlock);
Assert.True(playoutInitialization > safeMessage);
Assert.True(automaticConnectGuard > playoutInitialization);
Assert.True(automaticConnect > automaticConnectGuard);
Assert.True(earlyBlock >= 0);
Assert.True(earlyReturn > earlyBlock);
Assert.True(createEngine > earlyReturn);
}
[Fact]

View File

@@ -70,7 +70,7 @@ public sealed class LegacyFirstRunSetupNativeContractTests
}
[Fact]
public void BothFolderSelectionsValidateOnlyCanonicalPaths()
public void BothFolderSelectionsValidateCanonicalPathsAndTheSelectedDatabaseIni()
{
var setup = File.ReadAllText(Path.Combine(
AppRoot,
@@ -84,8 +84,11 @@ public sealed class LegacyFirstRunSetupNativeContractTests
"LegacyOperatorSettingsFolderKind.Resource",
setup,
StringComparison.Ordinal);
Assert.DoesNotContain("MmoneyCoder.ini", setup, StringComparison.Ordinal);
Assert.DoesNotContain("LegacyIniDatabaseOptionsLoader", setup,
Assert.Contains(
"Path.Combine(resource, \"MmoneyCoder.ini\")",
setup,
StringComparison.Ordinal);
Assert.Contains("LegacyIniDatabaseOptionsLoader", setup,
StringComparison.Ordinal);
var clearCuts = setup.IndexOf(
"_cutsDirectory = null;",
@@ -116,29 +119,78 @@ public sealed class LegacyFirstRunSetupNativeContractTests
}
[Fact]
public void SetupPersistsOnlyTheSelectedPathsWithoutStartingAChildProcess()
public void SetupRunsTheVerifiedInitializerAndVerifiesEveryDurableOutput()
{
var setup = File.ReadAllText(Path.Combine(
AppRoot,
"FirstRunSetupWindow.xaml.cs"));
var saveSettings = RequiredIndex(
setup,
"var saved = store.Save(current with");
var saveBinding = RequiredIndex(
setup,
"SaveRuntimeBinding(repositoryRoot, runtimeRoot);");
Assert.True(saveSettings < saveBinding);
Assert.Contains(
"var store = new LegacyOperatorSettingsStore();",
"await RunVerifiedSetupAsync(",
setup,
StringComparison.Ordinal);
Assert.Contains(
"SceneDirectory = cutsDirectory",
"new ProcessStartInfo",
setup,
StringComparison.Ordinal);
Assert.Contains(
"ResourceDirectory = resourceDirectory",
"UseShellExecute = false",
setup,
StringComparison.Ordinal);
Assert.Contains(
"CreateNoWindow = true",
setup,
StringComparison.Ordinal);
Assert.Contains(
"RedirectStandardOutput = true",
setup,
StringComparison.Ordinal);
Assert.Contains(
"RedirectStandardError = true",
setup,
StringComparison.Ordinal);
Assert.Contains(
"startInfo.ArgumentList.Add(value)",
setup,
StringComparison.Ordinal);
Assert.Contains(
"Initialize-ExistingDevelopmentPc.ps1",
setup,
StringComparison.Ordinal);
Assert.Contains(
"AddArgument(\"-SkipDatabaseProfile\")",
setup,
StringComparison.Ordinal);
Assert.Contains(
"AddArgument(\"-ConfigureDevelopmentLive\")",
setup,
StringComparison.Ordinal);
Assert.Contains(
"AddArgument(\"127.0.0.1\")",
setup,
StringComparison.Ordinal);
Assert.Contains(
"AddArgument(\"30001\")",
setup,
StringComparison.Ordinal);
Assert.Contains(
"AddArgument(\"-PinRegisteredK3D\")",
setup,
StringComparison.Ordinal);
Assert.Contains(
"DrainOutputAsync(process.StandardOutput)",
setup,
StringComparison.Ordinal);
Assert.Contains(
"DrainOutputAsync(process.StandardError)",
setup,
StringComparison.Ordinal);
Assert.Contains(
"loaded.Settings.SceneDirectory",
setup,
StringComparison.Ordinal);
Assert.Contains(
"loaded.Settings.ResourceDirectory",
setup,
StringComparison.Ordinal);
Assert.Contains(
@@ -146,18 +198,21 @@ public sealed class LegacyFirstRunSetupNativeContractTests
setup,
StringComparison.Ordinal);
Assert.Contains(
"File.Move(temporary, destination, overwrite: true);",
"\"k3d-pins.local.json\"",
setup,
StringComparison.Ordinal);
Assert.DoesNotContain("ProcessStartInfo", setup, StringComparison.Ordinal);
Assert.DoesNotContain("Process.Start", setup, StringComparison.Ordinal);
Assert.DoesNotContain("RunSetupAsync", setup, StringComparison.Ordinal);
Assert.DoesNotContain("powershell.exe", setup, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("dotnet", setup, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("ConfigureDevelopmentLive", setup,
Assert.Contains(
"\"playout.local.json\"",
setup,
StringComparison.Ordinal);
Assert.Contains(
"\"playout.development-live.local.json\"",
setup,
StringComparison.Ordinal);
Assert.DoesNotContain(
"Environment.SetEnvironmentVariable",
setup,
StringComparison.Ordinal);
Assert.DoesNotContain("PinRegisteredK3D", setup, StringComparison.Ordinal);
Assert.DoesNotContain("DatabaseIniPath", setup, StringComparison.Ordinal);
}
[Fact]
@@ -180,6 +235,19 @@ public sealed class LegacyFirstRunSetupNativeContractTests
StringComparison.Ordinal);
}
[Fact]
public void SolutionDeclaresTheVisualStudioWinUiWorkload()
{
var configuration = File.ReadAllText(Path.Combine(
RepositoryRoot,
".vsconfig"));
Assert.Contains(
"\"Microsoft.VisualStudio.Workload.Universal\"",
configuration,
StringComparison.Ordinal);
}
[Fact]
public void AutomaticK3dPinningIsPersistentAndCannotApproveAChangedDll()
{
@@ -277,11 +345,4 @@ public sealed class LegacyFirstRunSetupNativeContractTests
throw new DirectoryNotFoundException("Repository root was not found.");
}
private static int RequiredIndex(string value, string marker)
{
var index = value.IndexOf(marker, StringComparison.Ordinal);
Assert.True(index >= 0, $"Required marker was not found: {marker}");
return index;
}
}

View File

@@ -97,6 +97,11 @@ public sealed class LegacyOperatorSettingsNativeContractTests
Assert.Contains("_appliedOperatorSettings = operatorSettingsLoad.Settings", Window,
StringComparison.Ordinal);
Assert.Contains("ResolveFromResourceDirectory(", Window, StringComparison.Ordinal);
Assert.Contains(
"DatabaseRuntime.CreateDevelopmentLiveLocalOverride(\n" +
" selectedResourceDirectory)",
Window.ReplaceLineEndings("\n"),
StringComparison.Ordinal);
Assert.Contains("preferLegacyOverride: selectedLegacyDatabaseIni is not null", Window,
StringComparison.Ordinal);
Assert.Contains(
@@ -128,13 +133,18 @@ public sealed class LegacyOperatorSettingsNativeContractTests
databaseComposition,
"DatabaseRuntime.CreateDefault()",
gateA);
var selectedDevelopmentLiveIni = RequiredIndex(
databaseComposition,
"DatabaseRuntime.CreateDevelopmentLiveLocalOverride(",
pinnedJson);
var legacyIni = RequiredIndex(
databaseComposition,
"DatabaseRuntime.CreateLegacyExecutableDefault(",
pinnedJson);
selectedDevelopmentLiveIni);
Assert.True(gateA < pinnedJson);
Assert.True(pinnedJson < legacyIni);
Assert.True(pinnedJson < selectedDevelopmentLiveIni);
Assert.True(selectedDevelopmentLiveIni < legacyIni);
}
[Fact]

View File

@@ -373,7 +373,11 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"External legacy runtime paths are enabled",
StringComparison.Ordinal));
Assert.Contains(
"Cuts files remain in the selected folder",
@"Cuts and the credential-bearing Res\MmoneyCoder.ini remain",
fullRuntimeMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
Assert.Contains(
"in the selected external folders",
fullRuntimeMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
@@ -654,6 +658,10 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
RepositoryRoot,
"scripts",
"Initialize-ExistingDevelopmentPc.ps1"));
var registrationInspector = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Inspect-K3DRegistration.ps1"));
Assert.Contains("[string] $LegacyRuntimeSourceRoot", initializer,
StringComparison.Ordinal);
@@ -784,6 +792,34 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"[string] $classReport.ThreadingModel -cne 'Apartment'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$standardK3dRoot = 'C:\\K3DAsyncEngine'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"DLL\\x64\\Release\\K3DAsyncEngine.dll",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Bin\\x64\\C#\\Interop.K3DAsyncEngineLib.dll",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"-Right $standardK3dNativePath",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$expectedVendorRoot = 'C:\\K3DAsyncEngine'",
registrationInspector,
StringComparison.Ordinal);
Assert.Contains(
"-Expected $expectedNativePath",
registrationInspector,
StringComparison.Ordinal);
Assert.Contains(
"Assert-Amd64File -Path $interopPath",
registrationInspector,
StringComparison.Ordinal);
Assert.DoesNotContain("Activator.CreateInstance", initializer,
StringComparison.Ordinal);
Assert.DoesNotContain("Connect(", initializer, StringComparison.Ordinal);

View File

@@ -78,22 +78,22 @@ public sealed class InstalledK3dInteropMetadataTests
}
[Fact]
public void RegisteredPaths_MapOnlyExactX64VendorLayout()
public void RegisteredPaths_MapOnlyCompanyStandardInstallation()
{
const string native = @"C:\Vendor\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll";
const string native = K3dComConstants.StandardNativeBinaryPath;
var paths = InstalledK3dInteropMetadata.ResolveRegisteredPaths(ReadySnapshot(native));
Assert.Equal(Path.GetFullPath(native), paths.NativeBinaryPath);
Assert.Equal(
Path.GetFullPath(
@"C:\Vendor\K3DAsyncEngine\Bin\x64\C#\Interop.K3DAsyncEngineLib.dll"),
Path.GetFullPath(K3dComConstants.StandardInteropAssemblyPath),
paths.InteropAssemblyPath);
Assert.Equal(@"C:\K3DAsyncEngine", K3dComConstants.StandardInstallationRoot);
}
[Fact]
public void RegisteredPaths_RejectDivergentClassBinary()
{
const string native = @"C:\Vendor\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll";
const string native = K3dComConstants.StandardNativeBinaryPath;
var snapshot = ReadySnapshot(native) with
{
EventHandler = ReadySnapshot(native).EventHandler with
@@ -116,6 +116,16 @@ public sealed class InstalledK3dInteropMetadataTests
() => InstalledK3dInteropMetadata.ResolveRegisteredPaths(ReadySnapshot(native)));
}
[Fact]
public void RegisteredPaths_RejectAlternateRootWithMatchingX64Layout()
{
const string native =
@"D:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll";
Assert.Throws<InstalledK3dInteropMetadataUnavailableException>(
() => InstalledK3dInteropMetadata.ResolveRegisteredPaths(ReadySnapshot(native)));
}
private static K3dRegistrySnapshot ReadySnapshot(string nativePath) => new(
true,
nativePath,

View File

@@ -5,9 +5,9 @@ namespace MBN_STOCK_WEBVIEW.Playout.Tests;
public sealed class K3dRegistrationProbeTests
{
private const string TypeLibraryPath = "C:\\vendor-secret\\typelib-x64.dll";
private const string EnginePath = "C:\\vendor-secret\\engine-x64.dll";
private const string EventHandlerPath = "C:\\vendor-secret\\events-x64.dll";
private const string TypeLibraryPath = K3dComConstants.StandardNativeBinaryPath;
private const string EnginePath = K3dComConstants.StandardNativeBinaryPath;
private const string EventHandlerPath = K3dComConstants.StandardNativeBinaryPath;
[Fact]
public void Probe_WithReciprocalAmd64MachineRegistration_IsReady()
@@ -72,6 +72,32 @@ public sealed class K3dRegistrationProbeTests
AssertSafe(report);
}
[Fact]
public void Probe_WhenRegistryUsesAlternateRoot_FailsClosed()
{
const string alternate =
@"D:\K3DAsyncEngine\DLL\x64\Release\K3DAsyncEngine.dll";
var snapshot = ReadySnapshot() with
{
Win64TypeLibraryPath = alternate,
Engine = ReadySnapshot().Engine with { InprocServerPath = alternate },
EventHandler = ReadySnapshot().EventHandler with { InprocServerPath = alternate }
};
var binaries = new FakeBinaryInspector()
.Set(alternate, exists: true, isAmd64: true);
var probe = CreateProbe(snapshot, binaries);
var report = probe.Probe();
Assert.False(report.IsReady);
Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.TypeLibraryPathMismatch));
Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineServerPathMismatch));
Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerServerPathMismatch));
Assert.False(report.IsEngineClassReady);
Assert.False(report.IsEventHandlerClassReady);
AssertSafe(report);
}
[Fact]
public void Probe_WhenCurrentUserOverrideExists_FailsClosedWithoutDetails()
{