fix: initialize selected runtime for live playout
This commit is contained in:
6
.vsconfig
Normal file
6
.vsconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.Universal"
|
||||
]
|
||||
}
|
||||
44
README.md
44
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은
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 흔적을 담을 수 있어 어떤 출력/패키지에도 넣지 않는다. 실제 자산과 설정은
|
||||
|
||||
@@ -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 중립 인터페이스와 안전한 설정 예시만 유지합니다.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]@{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 송출 구성 요소가 준비되었습니다.",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user