diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..ba84d58 --- /dev/null +++ b/.vsconfig @@ -0,0 +1,6 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Workload.Universal" + ] +} diff --git a/README.md b/README.md index 38945f4..960a6df 100644 --- a/README.md +++ b/README.md @@ -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은 diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 95891fe..188a346 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -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 diff --git a/docs/DEVELOPMENT_LIVE_HANDOFF.md b/docs/DEVELOPMENT_LIVE_HANDOFF.md index 02ab674..8d320bb 100644 --- a/docs/DEVELOPMENT_LIVE_HANDOFF.md +++ b/docs/DEVELOPMENT_LIVE_HANDOFF.md @@ -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 '' ` -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 '' ` -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` | diff --git a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md index da1922f..28018c7 100644 --- a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md +++ b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md @@ -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 diff --git a/docs/LEGACY_RUNTIME_ASSETS.md b/docs/LEGACY_RUNTIME_ASSETS.md index d581168..4a708b3 100644 --- a/docs/LEGACY_RUNTIME_ASSETS.md +++ b/docs/LEGACY_RUNTIME_ASSETS.md @@ -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 흔적을 담을 수 있어 어떤 출력/패키지에도 넣지 않는다. 실제 자산과 설정은 diff --git a/docs/PLAYOUT.md b/docs/PLAYOUT.md index 2514843..4e0738d 100644 --- a/docs/PLAYOUT.md +++ b/docs/PLAYOUT.md @@ -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 = '' -$approvedInterop = '' +$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 = '\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 중립 인터페이스와 안전한 설정 예시만 유지합니다. diff --git a/scripts/Initialize-ExistingDevelopmentPc.ps1 b/scripts/Initialize-ExistingDevelopmentPc.ps1 index e0564da..cf5c865 100644 --- a/scripts/Initialize-ExistingDevelopmentPc.ps1 +++ b/scripts/Initialize-ExistingDevelopmentPc.ps1 @@ -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( diff --git a/scripts/Inspect-K3DRegistration.ps1 b/scripts/Inspect-K3DRegistration.ps1 index 2230742..56d029d 100644 --- a/scripts/Inspect-K3DRegistration.ps1 +++ b/scripts/Inspect-K3DRegistration.ps1 @@ -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]@{ diff --git a/src/MBN_STOCK_WEBVIEW.Infrastructure/DatabaseRuntime.cs b/src/MBN_STOCK_WEBVIEW.Infrastructure/DatabaseRuntime.cs index 1cbd0a1..a2d032e 100644 --- a/src/MBN_STOCK_WEBVIEW.Infrastructure/DatabaseRuntime.cs +++ b/src/MBN_STOCK_WEBVIEW.Infrastructure/DatabaseRuntime.cs @@ -63,24 +63,37 @@ public sealed class DatabaseRuntime } /// - /// 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. /// - 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( diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml index a6d926e..85406c0 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml @@ -23,7 +23,7 @@ @@ -66,7 +66,7 @@ Text="2. 설정/INI 폴더 (Res)" /> diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs index 4d16f65..91f6a2b 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs @@ -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) 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 81d7d43..a8bbe2d 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj @@ -185,8 +185,8 @@ @@ -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." /> + Text="External legacy runtime paths are enabled. Cuts and the credential-bearing Res\MmoneyCoder.ini remain in the selected external folders." /> @@ -242,8 +242,8 @@ diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs index a33d7ab..109412b 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs @@ -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 diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Diagnostics/InstalledK3dInteropConnectBinding.cs b/src/MBN_STOCK_WEBVIEW.Playout/Diagnostics/InstalledK3dInteropConnectBinding.cs index a8754e6..af4966c 100644 --- a/src/MBN_STOCK_WEBVIEW.Playout/Diagnostics/InstalledK3dInteropConnectBinding.cs +++ b/src/MBN_STOCK_WEBVIEW.Playout/Diagnostics/InstalledK3dInteropConnectBinding.cs @@ -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) diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Interop/K3dComConstants.cs b/src/MBN_STOCK_WEBVIEW.Playout/Interop/K3dComConstants.cs index 98915f6..359bb17 100644 --- a/src/MBN_STOCK_WEBVIEW.Playout/Interop/K3dComConstants.cs +++ b/src/MBN_STOCK_WEBVIEW.Playout/Interop/K3dComConstants.cs @@ -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"; diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Registration/K3dRegistrationProbe.cs b/src/MBN_STOCK_WEBVIEW.Playout/Registration/K3dRegistrationProbe.cs index b2b0c69..25b5993 100644 --- a/src/MBN_STOCK_WEBVIEW.Playout/Registration/K3dRegistrationProbe.cs +++ b/src/MBN_STOCK_WEBVIEW.Playout/Registration/K3dRegistrationProbe.cs @@ -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 송출 구성 요소가 준비되었습니다.", diff --git a/tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/LegacyIniDatabaseOptionsLoaderTests.cs b/tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/LegacyIniDatabaseOptionsLoaderTests.cs index 6974fc5..50392b5 100644 --- a/tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/LegacyIniDatabaseOptionsLoaderTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/LegacyIniDatabaseOptionsLoaderTests.cs @@ -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 + { + ["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( + () => 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( + () => 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( () => 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( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( - localApplicationData.Path)); + localApplicationData.Path, + operatorResourceDirectory: null)); } [Fact] @@ -296,7 +464,8 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests Assert.Throws( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( - localApplicationData.Path)); + localApplicationData.Path, + operatorResourceDirectory: null)); } [Fact] diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs index ce0bd9c..926d2ed 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs @@ -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] diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs index a4a2f9f..604b120 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs @@ -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; - } } diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs index e736eb4..7368309 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs @@ -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] diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs index dd27b66..600fcbe 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs @@ -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); diff --git a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/InstalledK3dInteropMetadataTests.cs b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/InstalledK3dInteropMetadataTests.cs index 7e1760c..597d9d9 100644 --- a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/InstalledK3dInteropMetadataTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/InstalledK3dInteropMetadataTests.cs @@ -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( + () => InstalledK3dInteropMetadata.ResolveRegisteredPaths(ReadySnapshot(native))); + } + private static K3dRegistrySnapshot ReadySnapshot(string nativePath) => new( true, nativePath, diff --git a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/K3dRegistrationProbeTests.cs b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/K3dRegistrationProbeTests.cs index 9d1702e..2f1076f 100644 --- a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/K3dRegistrationProbeTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/K3dRegistrationProbeTests.cs @@ -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() {