diff --git a/.gitignore b/.gitignore index 7b07424..b4103ef 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,8 @@ Config/playout*.local.json **/playout*.local.json Config/runtime-folders.local.json **/runtime-folders.local.json +Config/k3d-pins.local.json +**/k3d-pins.local.json *.local.ini Directory.Build.local.props .env diff --git a/README.md b/README.md index 095a8ce..49d3a3e 100644 --- a/README.md +++ b/README.md @@ -54,60 +54,70 @@ Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑 ## Visual Studio 2026에서 실행 -실제 송출을 확인할 기존 개발 PC는 바로 F5하지 말고 아래의 -`기존 설치가 있는 개발 PC에서 실제 송출` 초기화를 먼저 실행합니다. 화면만 안전하게 확인하는 -새 PC는 다음 순서를 사용합니다. +상사 PC처럼 기존 코더 자산과 K3D가 설치된 개발 PC에서는 별도 PowerShell 명령을 먼저 실행할 +필요가 없습니다. 1. `MBN_STOCK_WEBVIEW.sln`을 엽니다. 2. 솔루션 구성을 `Debug`, 플랫폼을 `x64`로 선택합니다. 3. 공유 시작 프로필 `Legacy Parity App (VS F5)` 또는 `src\MBN_STOCK_WEBVIEW.LegacyParityApp`을 시작 프로젝트로 선택합니다. -4. 실행 프로필이 `MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)`인지 확인합니다. +4. 실행 프로필이 + `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`인지 확인합니다. 5. `F5`로 빌드·배포·실행합니다. +6. 첫 실행 설정 창에서 기존 코더의 `Cuts` 폴더와 여러 INI가 함께 있는 `Res` 폴더, 두 + 폴더만 각각 선택하고 `설정 시작`을 누릅니다. +7. 이 앱의 이전 로컬 설정이 다른 경우에만 업데이트 확인 창이 한 번 표시됩니다. 선택한 + 두 폴더와 기본 Tornado2 설정으로 갱신하려면 `업데이트`를 누릅니다. K3D 기준값은 이 + 확인으로 바뀌지 않습니다. +8. `설정 완료`가 표시되면 창을 닫고 `F5`를 한 번 더 누릅니다. -새로 받은 소스에 외부 `MBN_STOCK_N\...\bin\Debug`가 없어도 Debug/F5는 안전한 -**소스 전용 DryRun**으로 빌드됩니다. 이 모드에서는 검증된 내장 메뉴로 화면을 확인할 수 있지만 -DB 검색과 실제 장면 송출은 사용할 수 없습니다. Test/Live 송출 안전 게이트는 완화되지 않으며, -소스 전용 빌드는 과거 로컬 송출 프로필·환경 변수·Development Live 실행 인수를 무시하고 -DryRun과 DB 미연결을 강제합니다. 출력과 중간 패키징 파일도 이전 전체 빌드의 `Cuts`, `Res` -또는 DB 설정을 잘못 재사용하지 않도록 `bin\SourceOnly`와 `obj\SourceOnly`에 분리됩니다. +새로 받은 clone에 아직 이 PC의 검증된 runtime 연결이 없으면, 주변에 과거 +`MBN_STOCK_N\...\bin\Debug`가 있더라도 첫 F5는 안전한 +**소스 전용 첫 실행 설정 앱**으로 빌드됩니다. 이 첫 프로세스는 과거 로컬 송출 프로필·환경 +변수·Development Live 실행 인수를 무시하고 DB 미연결과 DryRun을 강제하므로 설정 중에는 +Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중간 패키징 파일도 이전 전체 +빌드의 `Cuts`, `Res` 또는 DB 설정을 잘못 재사용하지 않도록 `bin\SourceOnly`와 +`obj\SourceOnly`에 분리됩니다. ### 기존 설치가 있는 개발 PC에서 실제 송출 상사 PC처럼 `Cuts`, `Res\MmoneyCoder.ini`, Tornado2/K3D x64와 장비 라이선스가 이미 있는 -**방송 비연결 개발 장비**에서는 Git에 자산을 넣거나 runtime ZIP을 다시 전달할 필요가 없습니다. -다만 Git은 이 로컬 항목과 실제 PGM endpoint·승인 해시를 의도적으로 포함하지 않으므로, -무설정 첫 F5만으로 실제 송출하는 것은 불가능합니다. 잘못된 과거 출력이나 다른 Tornado 자산을 -자동 선택하지 않도록 최초 clone마다 Visual Studio를 열기 전에 저장소 루트에서 다음 초기화를 -한 번 실행합니다. +지정 개발 장비에서는 Git에 자산·DB 설정·DLL·라이선스를 넣거나 runtime ZIP을 다시 전달할 +필요가 없습니다. 첫 실행 창에는 다음 두 폴더만 입력합니다. -```powershell -powershell -NoProfile -ExecutionPolicy Bypass ` - -File .\scripts\Initialize-ExistingDevelopmentPc.ps1 ` - -LegacyRuntimeSourceRoot '' ` - -ConfigureDevelopmentLive ` - -PlayoutHost 127.0.0.1 ` - -PlayoutPort ` - -NativeSha256 '<독립 승인된 native DLL SHA-256>' ` - -InteropSha256 '<독립 승인된 Interop DLL SHA-256>' -``` +- `Cuts`: 실제 실행 자산의 `Cuts` 폴더 +- `설정/INI (Res)`: `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, + `업종_코스닥.ini`, `해외.ini`, `환율.ini`, `지수.ini`, `종목비교.ini`가 함께 있는 + 실제 `Res` 폴더 -`-LegacyRuntimeSourceRoot`를 생략하면 정확한 기본 형제 경로를 먼저 확인하고, 없으면 Windows -폴더 선택기를 엽니다. 여기서는 `Cuts`와 `Res`를 함께 포함하는 `bin\Debug` 공통 부모를 -선택하거나, 편의를 위해 그 바로 아래의 `Cuts` 또는 `Res`를 선택해도 공통 부모로 -정규화됩니다. DB INI가 선택한 `Res\MmoneyCoder.ini`가 아닌 다른 위치에 있으면 검토한 -절대 경로를 `-DatabaseIniPath`로 지정합니다. 기존 승인 프로필이 별도 출력 채널을 사용하는 -경우에만 확인된 숫자를 `-OutputChannel`로 추가합니다. +개별 INI를 따로 고르는 입력은 없습니다. 첫 실행 설정이 위 **활성 INI 8개**를 모두 +확인하며, `MmoneyCoder.ini`는 DB 설정으로, 나머지 7개는 컷/UI 구성 정보로 처리합니다. +두 폴더는 같은 공통 부모 아래에 나란히 있어야 하며 폴더 이름도 각각 `Cuts`, `Res`여야 +합니다. 소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더가 아니라 기존 코더가 실제 +실행에 사용하던 `bin\Debug\Cuts`, `bin\Debug\Res` 성격의 폴더를 선택합니다. 초기화기는 기존 자산을 검증한 뒤 사용자 전용 LocalAppData에 검증된 runtime 복사본을 만들고, -Git 제외 `Directory.Build.local.props`가 그 복사본을 `Required` 모드로 사용하게 합니다. +그 경로를 명시해 전체 Debug 빌드를 먼저 검증합니다. Git 제외 +`Directory.Build.local.props`는 DB/K3D/Development Live 설정까지 성공한 뒤 마지막 성공 +표식으로 기록되어, 중간에 Visual Studio가 종료되면 다음 F5도 첫 실행 설정 화면으로 +돌아옵니다. DB INI도 사용자 전용 LocalAppData overlay로 복사해 ACL을 보호하고, K3D x64 등록을 읽기 -전용으로 점검합니다. 위 Live 옵션을 모두 준 경우에만 별도 승인 해시를 실제 등록 파일과 -대조한 뒤 보호된 Development Live 설정을 마지막에 새로 발급합니다. Debug x64 빌드 출력은 -선택한 manifest와 경로·길이·SHA-256·파일 집합이 정확히 일치하고 SourceOnly가 아닌지까지 -자동 확인합니다. 중간 단계가 실패하면 기존 runtime 연결과 DB overlay를 원래 내용·ACL로 -되돌리고 Live 승인 파일을 남기지 않습니다. 이 과정은 Tornado에 `CONNECT`하거나 장면을 -송출하지 않습니다. +전용으로 점검합니다. 예상 HKLM x64 등록·CLSID·ProgID·TypeLib·vendor 배치가 모두 맞는 +설치본의 두 DLL 해시를 이 PC의 최초 기준값으로 자동 고정하고, 이후 파일이 바뀌면 +Development Live를 차단합니다. 기준값은 사용자 전용 +`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json`에 지속 저장되며 Git에는 +들어가지 않습니다. 지정 개발 환경의 Tornado2는 별도 입력 없이 회사 기본값 +`127.0.0.1:30001`로 구성합니다. 보호된 Development Live 설정은 모든 검증이 성공한 +마지막 기능 설정 단계에서만 발급되고, 그 뒤 로컬 build binding이 성공 표식으로 기록됩니다. + +테스트 개발 서버라 자격증명 노출 위험을 수용하는 경우에도 저장소 계약은 바뀌지 않습니다. +`MmoneyCoder.ini`와 DB 자격증명은 사용자 전용 LocalAppData에만 두고 Git, 빌드 출력, +runtime bundle과 MSIX에는 포함하지 않습니다. + +Debug x64 빌드 출력은 선택한 manifest와 경로·길이·SHA-256·파일 집합이 정확히 일치하고 +SourceOnly가 아닌지까지 자동 확인합니다. 중간 단계가 실패하면 기존 runtime 연결과 DB +overlay를 원래 내용·ACL로 되돌리고 Live 승인 파일을 남기지 않습니다. 이 과정은 Tornado에 +`CONNECT`하거나 장면을 송출하지 않습니다. 초기화가 성공하면 Visual Studio에서 `Debug|x64`, `Legacy Parity App (VS F5)`, `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작합니다. @@ -120,10 +130,18 @@ endpoint는 숫자형 loopback, scene root는 실행 파일 옆 검증된 `Cuts` 검증이 실패하면 Live를 승인하지 않고 안전한 DryRun으로 시작합니다. 같은 clone에서는 로컬 연결 파일이 Git에 덮어써지지 않으므로 이후에는 `git pull` 후 F5로 -확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB·K3D·PGM 설정이 바뀌면 -초기화를 다시 실행합니다. pull 뒤 Required 자산 검증이 실패하는 경우에도 F5를 계속 시도하지 -말고 초기화기를 다시 실행합니다. 상사에게 저장소 주소를 전달하기 전에는 이 초기화 기능이 -상사가 받는 기본 브랜치에 통합되어 있는지 확인합니다. +확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB 폴더 연결이 바뀌면 첫 실행 +설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 첫 실행 설정을 반복해도 자동 +재승인되지 않습니다. 벤더 변경을 독립적으로 검증한 뒤 두 SHA-256을 명시해 수동 교체해야 +합니다. pull 뒤 Required 자산 검증이 실패하는 경우에도 F5를 계속 시도하지 말고 설정 오류를 +바로잡습니다. + +화면을 열 수 없는 장애 대응이나 기존 로컬 설정의 명시적 교체가 필요할 때만 +`scripts\Initialize-ExistingDevelopmentPc.ps1`을 수동 도구로 사용합니다. 일반적인 새 +clone 인수 절차에서는 이 스크립트를 직접 실행하지 않습니다. K3D 기준 교체의 정확한 +`-ReplaceK3DPin` 절차는 +[개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md#k3d-dll이-정식으로-교체된-경우)에 +따릅니다. 해당 PC에 `Cuts`/`Res`가 없을 때만 `New-LegacyRuntimeBundle.ps1`과 `Initialize-LegacyRuntimeBundle.ps1`의 검증 ZIP 절차를 diff --git a/docs/DEVELOPMENT_LIVE_HANDOFF.md b/docs/DEVELOPMENT_LIVE_HANDOFF.md index 1107b3f..ad1770e 100644 --- a/docs/DEVELOPMENT_LIVE_HANDOFF.md +++ b/docs/DEVELOPMENT_LIVE_HANDOFF.md @@ -17,19 +17,24 @@ 상사 PC에 이미 `Cuts`, `Res\MmoneyCoder.ini`, Tornado2/K3D x64와 장비 라이선스가 있다면 같은 항목을 다시 전달하지 않는다. 대신 기존 설치 위치가 이번 인수 대상이 맞는지 확인하고, -다음 값은 저장소가 아니라 승인된 사내 기록이나 전달 경로에서 확인한다. +다음 항목을 확인한다. - DB endpoint와 계정 정보. 메신저 화면, 작업 기록 또는 Git에 값을 남기지 않는다. -- 기존 `Cuts`와 `Res`의 공통 부모 경로, 그리고 실제 자격증명 INI 경로. +- 기존 코더가 실제 실행에 사용하던 서로 이웃한 `Cuts`와 `Res` 폴더. - Tornado2/K3D x64 설치본과 해당 장비용 라이선스 상태. -- 벤더 배포본 또는 관리자 검수로 **독립 승인된** native DLL과 Interop DLL의 SHA-256 두 개. -- 개발 PGM의 실제 Network Server TCP port, 창 제목, 출력 라우팅과 담당자 확인 정보. +- 개발 PGM의 로컬 Network Server가 회사 기본값 `127.0.0.1:30001`을 사용한다는 사실과 + 출력 라우팅. 해당 PC에 승인된 `Cuts`/`Res`가 없을 때만 `New-LegacyRuntimeBundle.ps1`로 생성한 runtime bundle ZIP과 별도 신뢰 경로로 전달할 기대 SHA-256을 추가로 준비한다. -현재 설치된 DLL의 해시를 그 자리에서 계산한 뒤 같은 값을 승인값으로 삼지 않는다. 라이선스 -파일, DLL, 인증서 또는 자산을 앱 폴더에 임의 복사해 등록·라이선스 검사를 우회하지 않는다. +첫 실행 설정은 예상 HKLM Registry64 등록, CLSID·ProgID·TypeLib, AMD64, vendor 폴더 배치와 +HKCU override 부재를 먼저 검증한다. 모두 맞을 때만 현재 설치된 native/Interop 두 DLL의 +SHA-256을 이 PC의 최초 기준으로 자동 고정한다. 이후 DLL이나 등록이 바뀌면 자동 재승인하지 +않고 Development Live를 차단한다. 이 지속 기준은 사용자 전용 +`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json`에 저장하며 Git에는 넣지 +않는다. 라이선스 파일, DLL, 인증서 또는 자산을 앱 폴더에 임의 복사해 등록·라이선스 검사를 +우회하지 않는다. ## 1. 개발 장비와 대상 고정 @@ -37,8 +42,8 @@ bundle ZIP과 별도 신뢰 경로로 전달할 기대 SHA-256을 추가로 준 1. 이 PC와 로컬 Tornado2/PGM은 개발용이며 실제 방송 라우팅에 영향이 없다. 2. `Tornado2*` 프로세스는 정확히 하나이고 주 창 제목은 승인값과 정확히 일치한다. -3. Tornado2의 `Tools > Option > Control > Network Server > TCP Port`에 표시된 값을 직접 - 읽는다. 문서나 예제의 `30001`을 추정값으로 사용하지 않는다. +3. Tornado2의 `Tools > Option > Control > Network Server > TCP Port`가 지정 개발 환경의 + 회사 기본값 `30001`인지 확인한다. 다른 값이면 자동 첫 실행 설정을 사용하지 않는다. 4. 해당 TCP LISTEN 소유 PID와 Tornado2 PID가 같고, 실행 도중 프로세스 세대가 바뀌지 않는다. 5. `View > Network Monitoring Window`와 PGM 화면을 동시에 관찰할 담당자가 있다. @@ -46,53 +51,58 @@ bundle ZIP과 별도 신뢰 경로로 전달할 기대 SHA-256을 추가로 준 ## 2. 기존 자산 보유 PC: clone 후 1회 초기화 -상사 PC처럼 승인된 자산과 벤더 설치가 이미 있는 경우에는 이 흐름을 우선 사용한다. 저장소를 -clone한 뒤 Visual Studio를 열기 전에 저장소 루트의 PowerShell에서 다음 초기화기를 한 번 -실행한다. +상사 PC처럼 승인된 자산과 벤더 설치가 이미 있는 경우에는 이 흐름을 우선 사용한다. 별도 +PowerShell 명령은 필요 없다. -```powershell -powershell -NoProfile -ExecutionPolicy Bypass ` - -File .\scripts\Initialize-ExistingDevelopmentPc.ps1 ` - -LegacyRuntimeSourceRoot '' ` - -ConfigureDevelopmentLive ` - -PlayoutHost 127.0.0.1 ` - -PlayoutPort ` - -NativeSha256 '<독립 승인된 native DLL SHA-256>' ` - -InteropSha256 '<독립 승인된 Interop DLL SHA-256>' -``` +1. 저장소를 clone하고 `MBN_STOCK_WEBVIEW.sln`을 Visual Studio 2026에서 연다. +2. `Debug|x64`, `Legacy Parity App (VS F5)`, + `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택한다. +3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB 미연결과 DryRun을 + 강제하고 Tornado2/PGM에 연결하지 않는다. +4. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다. +5. `Res 폴더 선택`에서 다음 파일들이 함께 있는 실제 설정 폴더를 선택한다. + `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`, `해외.ini`, + `환율.ini`, `지수.ini`, `종목비교.ini`. +6. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다. +7. `설정 완료`가 표시되면 창을 닫고 F5를 한 번 더 누른다. -DB INI가 `<공통 부모>\Res\MmoneyCoder.ini`가 아닌 다른 위치에 있다면 검토한 절대 경로를 -`-DatabaseIniPath`로 추가한다. `-LegacyRuntimeSourceRoot`를 생략했을 때는 스크립트가 -정확한 기본 형제 저장소 경로를 먼저 확인하고, 찾지 못하면 폴더 선택기를 연다. 선택기에서는 -`Cuts`와 `Res`를 함께 포함하는 `bin\Debug` 공통 부모를 선택한다. 그 바로 아래의 `Cuts` -또는 `Res`를 선택해도 스크립트가 공통 부모로 정규화한다. 이름이 비슷한 백업 폴더나 과거의 -다른 빌드 출력은 선택하지 않는다. 비대화형 실행에서 선택기를 금지하려면 -`-NoFolderPicker`를 추가하며, 이 경우 기본 형제 경로가 없으면 명시적 경로 없이 진행하지 -않는다. -기존 승인 프로필이 별도 출력 채널을 사용하는 경우에만 확인된 숫자를 -`-OutputChannel `로 추가하고, 기본 player를 사용하는 경우에는 생략한다. +소스 트리의 오래된 `RES`, 백업 폴더, `MmoneyCoder - 복사본.ini`가 있는 다른 위치를 +선택하지 않는다. 화면 입력은 `Cuts`와 `Res` 두 폴더뿐이지만, 첫 실행 앱은 +`MmoneyCoder.ini`를 포함한 **활성 INI 8개**를 모두 확인한다. 7개 UI INI는 각각 파싱하고 +`MmoneyCoder.ini`는 DB 계약을 검증한다. 전체 Res를 무차별 복사하지 않고 폐쇄형 34개 +allowlist만 runtime으로 구성하며, 자격증명 INI는 별도 사용자 전용 경로로만 복사한다. +개발 테스트 서버의 계정이라 노출을 허용하는 경우에도 자격증명을 Git이나 빌드 산출물에 넣지 +않는 계약은 그대로 유지한다. 초기화기는 다음 작업을 한 번에 수행해야 한다. - 로컬 `Cuts`/`Res` tree와 프로젝트가 요구하는 자산 계약을 읽기 전용으로 검증하고, 임시 bundle을 거쳐 사용자 전용 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\RuntimeBundles\`에 검증된 복사본을 설치한다. -- 저장소에서 제외되는 `Directory.Build.local.props`가 그 설치 root를 - `LegacyRuntimeAssetsMode=Required`로 사용하게 한다. +- 설치 root를 명시적 MSBuild 속성으로 전달해, 저장소 binding을 아직 바꾸지 않은 상태에서 + Debug x64 전체 자산 빌드를 먼저 검증한다. - 기존 DB INI를 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini`로 복사하고 현재 Windows 사용자 전용 ACL을 적용한다. -- K3D x64 등록 상태를 읽기 전용으로 점검한다. -- `-ConfigureDevelopmentLive`를 지정했을 때만 port와 독립 승인 해시를 검증하고 보호된 로컬 - Development Live 설정을 모든 선행 검증이 끝난 마지막 단계에서 새로 발급한다. -- 별도 `-SkipBuild`를 주지 않은 기본 실행에서는 Debug x64 전체 자산 빌드 후 선택된 runtime +- K3D x64 등록 상태를 읽기 전용으로 점검하고 검증된 설치본의 native/Interop SHA-256을 + 사용자 전용 `Config\k3d-pins.local.json`의 최초 기준으로 자동 고정한다. 이 파일이 이미 + 있으면 같은 값을 재사용하고, 등록 DLL이 달라졌을 때 자동 갱신하지 않는다. +- Debug x64 전체 자산 빌드 후 선택된 runtime root, Cuts/Res의 경로·길이·SHA-256·전체 파일 집합, SourceOnly가 아님과 DB INI가 빌드 출력에 들어가지 않았음을 검증한다. +- `127.0.0.1:30001` 개발 기본 endpoint의 보호된 로컬 Development Live 설정을 모든 선행 + 검증 뒤 새로 발급한다. +- 저장소에서 제외되는 `Directory.Build.local.props`를 마지막 성공 표식으로 원자 기록해 + 그 설치 root를 `LegacyRuntimeAssetsMode=Required`로 사용하게 한다. 초기화 과정 자체는 Tornado2/PGM에 `CONNECT`하거나 PREPARE, TAKE IN, NEXT, TAKE OUT 등의 명령을 보내지 않는다. 초기화 도중 오류가 나면 F5로 송출을 시도하지 말고 원인부터 바로잡는다. -`-ConfigureDevelopmentLive`와 `-SkipBuild`는 함께 사용할 수 없으므로 실제 송출 설정 전에 -전체 자산 빌드 검증을 생략할 수 없다. runtime 연결이나 DB overlay를 갱신한 뒤 선행 단계가 -실패하면 기존 파일의 정확한 내용과 ACL을 복구하고, Live 승인 파일은 제거된 상태로 둔다. +runtime 연결이나 DB overlay를 갱신한 뒤 선행 단계가 실패하면 기존 파일의 정확한 내용과 +ACL을 복구하고 Live 승인 파일은 제거된 상태로 둔다. 반면 K3D pin은 DLL 변경 뒤 재시도로 +자동 승인되는 일을 막는 지속 신뢰 기준이므로, 최초 생성 또는 독립 검증에 따른 명시적 교체가 +완료된 뒤 다른 설정 단계가 실패해도 그대로 보존한다. +`Directory.Build.local.props`는 모든 기능 설정이 끝난 뒤에만 기록하므로, 설정 창이나 +Visual Studio가 그 전에 비정상 종료되면 다음 F5도 SourceOnly 첫 실행 화면으로 돌아와 +같은 두 폴더로 다시 진행할 수 있다. 기본 초기화가 이미 빌드를 확인한다. 필요하면 같은 조건이 유지되는지 별도 MSBuild 인자나 환경 변수 없이 다음 명령으로 다시 확인할 수 있다. @@ -105,22 +115,35 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar 출력에 전체 runtime 자산이 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드, `bin\SourceOnly` 또는 `DryRun`만 표시되면 Development Live를 진행하지 않는다. -위 권장 명령의 최종 요약은 `K3DStatus=Valid`, `K3DComActivated=false`, -`BuildVerified=true`, `DevelopmentLiveRequested=true`여야 한다. -`DevelopmentLiveWritten=true`, `DevelopmentLiveReused=false`여야 한다. 경로와 해시가 포함된 -전체 출력은 Git이나 일반 메신저에 붙이지 않는다. - -같은 자산·DB·Live 설정으로 다시 실행하면 검증된 runtime과 DB는 재사용하지만, 기존 Live -승인은 먼저 무효화하고 전체 검증이 다시 성공한 뒤 새로 발급한다. 기존 값이 달라졌다면 자동으로 -덮어쓰지 않으며, 변경 대상을 먼저 검토한 뒤 필요한 항목에만 각각 -`-ReplaceRuntimeBinding`, `-ReplaceDatabaseProfile`, `-ReplaceLiveConfig`를 명시한다. - 같은 clone에서는 이후 `git pull` 뒤 Visual Studio 2026에서 `Debug|x64`, 시작 대상 `Legacy Parity App (VS F5)`, 실행 프로필 `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작할 수 -있다. 저장소를 새 폴더에 다시 clone하거나 자산·DB·K3D·PGM 설정이 바뀐 경우에는 초기화기를 -다시 실행한다. pull 뒤 Required 자산 검증이 실패하는 경우에도 F5를 반복하지 말고 초기화기를 -다시 실행한다. 최초 실제 동작 확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다. +있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 설정을 다시 진행한다. 자산·DB·K3D·PGM +설정이 바뀌었거나 pull 뒤 Required 자산 검증이 실패하면 F5를 반복하지 말고 변경 원인을 +검토한다. 특히 K3D DLL 변경은 첫 실행 화면을 반복해도 자동 승인되지 않는다. 최초 실제 동작 +확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다. + +### 화면 초기화를 사용할 수 없을 때 + +첫 실행 창을 열 수 없는 장애 대응에만 저장소 루트에서 다음 수동 등가 명령을 사용한다. + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass ` + -File .\scripts\Initialize-ExistingDevelopmentPc.ps1 ` + -LegacyRuntimeSourceRoot '' ` + -NoFolderPicker ` + -ConfigureDevelopmentLive ` + -PlayoutHost 127.0.0.1 ` + -PlayoutPort 30001 ` + -PinRegisteredK3D +``` + +기존 로컬 runtime, DB 또는 Live 설정과 다르면 자동으로 덮어쓰지 않는다. 변경 대상을 검토한 +뒤에만 `-ReplaceRuntimeBinding`, `-ReplaceDatabaseProfile`, `-ReplaceLiveConfig` 중 필요한 +항목을 명시한다. 수동 모드는 기존처럼 독립 승인된 `-NativeSha256`과 `-InteropSha256`을 +사용할 수도 있으며, 이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 이 스크립트는 첫 +실행 화면이 내부에서 사용하는 초기화 도구의 수동 fallback이며 일반 clone 절차의 선행 +명령이 아니다. ## 3. 자산이 없는 PC 대안: 검증된 Git 밖 runtime bundle 설치 @@ -257,7 +280,7 @@ powershell -NoProfile -ExecutionPolicy Bypass ` 검증으로 넘어가지 않는다. DB 연결 확인은 [DB 운영 가이드](DATABASE.md)의 read-only smoke만 먼저 수행하고, 실패 시 Tornado 명령을 보내지 않는다. -## 5. K3D 설치, 라이선스와 승인 해시 확인 +## 5. K3D 설치, 라이선스와 기준 해시 확인 벤더 절차로 Tornado2/K3D x64와 장비 라이선스를 먼저 설치한다. 저장소의 점검은 레지스트리와 파일을 읽을 뿐 COM을 활성화하지 않는다. 2단계 초기화는 이 검사를 항상 수행하고 `Valid`, @@ -271,23 +294,60 @@ powershell -NoProfile -ExecutionPolicy Bypass ` Registry64, TypeLib/CLSID 양방향 매핑, `Apartment`, AMD64 PE, HKCU override 부재 검사가 모두 성공해야 한다. 점검 성공은 라이선스 성공을 대신하지 않으므로 벤더 방식으로 라이선스도 별도 -확인한다. native와 Interop의 실제 파일은 각각 독립 승인된 SHA-256과 일치해야 하며 둘 중 하나라도 -다르면 중단한다. +확인한다. 첫 실행 설정은 이 검사를 통과한 native와 Interop 파일의 SHA-256을 최초 기준으로 +고정하며, 이후 실제 파일이 기준값과 하나라도 다르면 중단한다. 수동 승인 모드에서는 두 파일이 +각각 독립 승인된 SHA-256과 일치해야 한다. + +지속 pin 파일의 경로는 다음과 같다. + +```text +%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json +``` + +이 파일은 정확한 JSON 계약과 현재 Windows 사용자 전용 ACL로 원자적으로 생성된다. 최초 +기준이 고정된 뒤에는 나머지 설정의 성공 여부와 무관하게 파일을 삭제하거나 편집해 새 DLL을 +승인하지 않는다. + +### K3D DLL이 정식으로 교체된 경우 + +벤더 업데이트 등으로 DLL이 의도적으로 바뀌었다면 두 파일을 별도 배포 증적이나 관리자 검수로 +독립 확인한 뒤에만 수동 fallback을 사용한다. 현재 설치 파일에서 계산한 값만 보고 같은 +값을 승인하지 않는다. 일반적인 교체 명령은 다음과 같으며 +`-ReplaceK3DPin`은 `-PinRegisteredK3D`와 함께 사용할 수 없다. + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass ` + -File .\scripts\Initialize-ExistingDevelopmentPc.ps1 ` + -LegacyRuntimeSourceRoot '' ` + -NoFolderPicker ` + -ConfigureDevelopmentLive ` + -PlayoutHost 127.0.0.1 ` + -PlayoutPort 30001 ` + -NativeSha256 '' ` + -InteropSha256 '' ` + -ReplaceK3DPin ` + -ReplaceLiveConfig +``` + +runtime이나 DB 위치도 의도적으로 달라졌을 때만 각각 `-ReplaceRuntimeBinding`, +`-ReplaceDatabaseProfile`을 추가한다. 해시가 실제 등록 파일과 다르거나 K3D 등록 검사가 +실패하면 기존 pin을 보존하고 중단한다. ## 6. 보호된 로컬 송출 설정 -2단계 초기화에 `-ConfigureDevelopmentLive`와 필수 값을 주었다면 아래 설정 두 파일도 -마지막 단계에서 검증 후 새로 발급된다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug -빌드가 모두 성공하지 않으면 새 Live 승인 파일을 남기지 않는다. 자산 없는 PC의 3단계 bundle 흐름처럼 설정만 별도로 -초기화해야 할 때는 저장소 루트에서 다음 스크립트를 실행한다. port는 이번에 Tornado2 화면에서 -직접 확인한 값이고, 두 해시는 독립 승인값이어야 한다. 기존 승인 프로필이 별도 출력 채널을 -사용할 때만 검증된 숫자를 `-OutputChannel`로 추가하며, 기본 player를 쓰는 경우에는 생략한다. +2단계 첫 실행 설정은 지속 K3D pin을 확인한 뒤 아래 송출 설정 두 파일도 마지막 기능 설정 +단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug 빌드가 모두 +성공하지 않으면 새 Live 승인 파일을 남기지 않는다. 자산 없는 PC의 3단계 bundle 흐름처럼 +설정만 별도로 초기화해야 할 때는 저장소 루트에서 다음 스크립트를 실행한다. 이 수동 경로의 +두 해시는 별도로 확인한 값이어야 한다. 지정 개발 PC의 endpoint는 +`127.0.0.1:30001`로 고정한다. 기존 승인 프로필이 별도 출력 채널을 사용할 때만 검증된 숫자를 +`-OutputChannel`로 추가하며, 기본 player를 쓰는 경우에는 생략한다. ```powershell powershell -NoProfile -ExecutionPolicy Bypass ` -File .\scripts\Initialize-DevelopmentLiveConfig.ps1 ` -PlayoutHost 127.0.0.1 ` - -PlayoutPort ` + -PlayoutPort 30001 ` -NativeSha256 '' ` -InteropSha256 '' ``` @@ -309,7 +369,7 @@ powershell -NoProfile -ExecutionPolicy Bypass ` |---|---| | `mode` | 항상 `DryRun`. Development Live 프로세스만 일시적으로 Live로 덮어쓴다. | | `host` | 로컬 PGM이면 숫자형 loopback `127.0.0.1`. `localhost` 또는 원격 주소를 추정하지 않는다. | -| `port` | Tornado2 화면에서 이번에 직접 확인한 Network Server TCP port | +| `port` | 지정 개발 환경의 고정 Network Server TCP port `30001` | | `tcpMode` / `clientPort` | `1` / `0` | | `sceneDirectory` | `null`. 검증된 로컬 runtime이 빌드 출력에 배치한 기본 `Cuts`를 사용한다. | | `outputChannel` | 승인된 개발 PGM 라우팅 값. 기존 승인 프로필이 기본 player를 쓰는 경우에만 `null` | @@ -357,8 +417,10 @@ scene root는 실행 파일 옆 검증된 `Cuts`로 마지막에 다시 고정 } ``` -주석, trailing comma, 중복·추가 속성을 넣지 않는다. 두 해시와 승인 근거는 Git 밖에 보관한다. -현재 설치 파일에서 즉석 계산한 값으로 placeholder를 채우지 않는다. 자세한 strict 파일 규칙은 +주석, trailing comma, 중복·추가 속성을 넣지 않는다. 수동 승인 모드의 두 해시와 승인 근거는 +Git 밖에 보관하고 현재 설치 파일에서 즉석 계산한 값으로 placeholder를 채우지 않는다. 첫 실행 +설정은 지속 K3D pin을 먼저 검증한 뒤 그 기준값으로 이 Live 승인 파일을 생성한다. 자세한 +strict 파일 규칙은 [Visual Studio Development Live 설정](DEVELOPMENT_LIVE_VISUAL_STUDIO.md)을 따른다. ## 7. 정확한 로컬 PGM 연결 전용 확인 @@ -375,7 +437,7 @@ dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke ` --pgm-connect-diagnostic ` --i-understand-this-will-contact-current-pgm-tornado-via-ktap-connect-and-disconnect-only ` --host 127.0.0.1 ` - --port ` + --port 30001 ` --expected-pgm-window-title PGM ``` @@ -421,9 +483,9 @@ Live 앱을 종료하면 부트스트랩이 만든 Live 환경은 프로세스 - 지정 개발 PC와 정확한 로컬 PGM 대상 확인 - 기존 자산 또는 외부 bundle에서 설치한 검증된 로컬 runtime과 허용 scene 확인 -- K3D x64 등록, 장비 라이선스와 두 독립 승인 해시 확인 +- K3D x64 등록, 장비 라이선스, 지속 pin과 자동 고정 또는 수동 승인된 두 기준 해시 확인 - 개발 DB read-only smoke 성공 -- 두 `%LOCALAPPDATA%` 설정 파일의 내용·ACL 검증 +- 지속 K3D pin과 두 `%LOCALAPPDATA%` 송출 설정 파일의 내용·ACL 검증 - connect-only 진단과 최소 5001 회차가 retry 0, `OutcomeUnknown=false`로 종료 - 최종 TAKE OUT/IDLE, DISCONNECT와 Explicit DryRun 재시작 확인 - 누락 영상 14개 관련 action이 제한 상태임을 인수자에게 고지 @@ -438,7 +500,7 @@ Git 밖 인수 기록에만 보관한다. `Directory.Build.local.props` - `Res\MmoneyCoder.ini`, `database.local.json`과 DB 자격증명 - `playout.local.json`, `playout.development-live.local.json`, - `runtime-folders.local.json` + `runtime-folders.local.json`, `k3d-pins.local.json` - Tornado/K3D native DLL, Interop DLL, 라이선스 파일·키 - native/Interop 승인 해시와 승인 근거 - MSIX 서명 인증서, 개인 키와 암호 diff --git a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md index 69ea830..1024845 100644 --- a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md +++ b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md @@ -22,6 +22,22 @@ 없고, JSON 또는 해시 형식이 잘못된 경우에도 시작은 fail-closed로 진행되어 기존 `playout.local.json`의 안전 기본인 `DryRun`을 사용한다. +새 clone처럼 아직 검증된 runtime 연결이 없는 경우의 첫 F5는 예외적으로 소스 전용 설정 +앱을 연다. 이 프로세스는 `--development-live`를 적용하지 않고 DB와 송출을 모두 차단한다. +화면에서 기존 코더의 `Cuts`와 `Res` 폴더, 두 개만 선택한다. `Res` 안에서는 +`MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`, `해외.ini`, +`환율.ini`, `지수.ini`, `종목비교.ini` 등 활성 INI 8개를 모두 확인한다. 개별 INI나 +Tornado2 endpoint를 입력하지 않으며 endpoint는 지정 개발 환경 기본값 +`127.0.0.1:30001`로 고정된다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누른다. 두 번째 +F5부터 위 세 조건을 검증해 Development Live를 적용한다. 자세한 절차는 +[개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를 +따른다. + +`MmoneyCoder.ini`의 DB 자격증명은 테스트 개발 서버용이라도 Git에 넣지 않는다. 첫 실행 +설정은 사용자 전용 LocalAppData overlay로만 복사하며 빌드 출력, runtime bundle과 MSIX에는 +포함하지 않는다. 첫 실행 화면이 열리지 않거나 기존 로컬 설정을 명시적으로 교체해야 할 때만 +`Initialize-ExistingDevelopmentPc.ps1`을 수동 fallback으로 사용한다. + ## Visual Studio 시작 대상과 프로필 공유 솔루션 실행 프로필은 저장소 루트의 @@ -53,6 +69,26 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴 ## 로컬 승인 파일 +### 지속 K3D pin + +첫 실행 설정은 K3D의 HKLM x64 등록, CLSID·ProgID·TypeLib, AMD64와 vendor 배치를 먼저 +검증한다. 검사를 통과한 최초 native/Interop DLL의 SHA-256은 다음 사용자 전용 파일에 +지속 저장한다. + +```text +%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json +``` + +이후 등록 DLL의 해시가 이 기준과 다르면 첫 실행 설정을 다시 눌러도 자동으로 pin을 바꾸지 +않고 Development Live를 차단한다. 정식 DLL 교체 시에는 벤더 배포본 등으로 독립 검증한 +`-NativeSha256`과 `-InteropSha256`을 제공하고 +`Initialize-ExistingDevelopmentPc.ps1 -ReplaceK3DPin -ReplaceLiveConfig` 수동 절차를 +사용해야 한다. `-ReplaceK3DPin`과 현재 등록 파일을 자동 채택하는 `-PinRegisteredK3D`는 +함께 사용할 수 없다. 정확한 명령은 +[개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#k3d-dll이-정식으로-교체된-경우)에 있다. + +### Development Live 승인 + 실제 파일 경로는 다음과 같다. ```text @@ -86,9 +122,10 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴 - 파일 없음, 잘못된 경로, 공유 위반, I/O 또는 읽기 권한 오류는 모두 같은 제한된 `authorization-unavailable` 결과로 처리하며 경로와 값은 로그에 표시하지 않는다. -현재 설치 파일의 해시를 그 자리에서 계산했다는 이유만으로 승인 값으로 사용해서는 안 -된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 파일 ACL은 현재 -개발 사용자와 관리자만 수정할 수 있도록 유지한다. +수동 생성·교체 시에는 현재 설치 파일의 해시를 그 자리에서 계산했다는 이유만으로 승인 값으로 +사용해서는 안 된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 일반 +첫 실행에서는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤 같은 기준값으로 이 승인 파일을 +발급한다. 파일 ACL은 현재 개발 사용자와 관리자만 수정할 수 있도록 유지한다. ## 기존 playout.local.json diff --git a/docs/LEGACY_RUNTIME_ASSETS.md b/docs/LEGACY_RUNTIME_ASSETS.md index 7d3642a..b191486 100644 --- a/docs/LEGACY_RUNTIME_ASSETS.md +++ b/docs/LEGACY_RUNTIME_ASSETS.md @@ -26,15 +26,27 @@ Web\... 기본 `LegacyRuntimeAssetsMode`는 `Auto`다. -- 기본 형제 경로에 원본이 있으면 모든 필수 자산을 검증하고 전체 자산 빌드를 만든다. -- 원본이 없는 새 Git clone의 Debug/F5는 빌드를 막지 않고 소스 전용 DryRun을 만든다. +- 초기화기가 모든 설정과 명시 경로 전체 빌드를 검증한 뒤 마지막 성공 표식으로 만든 Git 제외 + `Directory.Build.local.props`가 있으면, 그 검증된 LocalAppData runtime으로 모든 필수 + 자산을 확인하고 전체 자산 빌드를 만든다. +- 새 Git clone의 Debug/F5는 주변의 과거 원본 경로를 자동 선택하지 않고 소스 전용 첫 실행 + 설정 앱을 만든다. - 원본 경로를 명시하거나 Release MSIX를 만들면 기본값이 `Required`로 바뀌어 누락 자산을 다시 오류로 차단한다. -소스 전용 앱은 내장 컷/UI 목록으로 화면 검토가 가능하고 DB는 미연결 상태로 표시한다. -과거 로컬 송출 프로필, 운영자 송출·배경 경로, 프로세스 환경 override와 Development Live 실행 -인수는 사용하지 않고 안전한 DryRun 기본값을 강제한다. 출력과 중간 패키징 파일은 이전 전체 빌드의 -자산이나 `MmoneyCoder.ini`를 재사용하지 않도록 다음 별도 경로에 생성된다. +소스 전용 앱은 메인 송출 화면 대신 `Cuts`와 `Res` 두 폴더만 받는 네이티브 첫 실행 창을 +표시한다. 이 프로세스는 DB를 초기화하지 않고 과거 로컬 송출 프로필, 운영자 송출·배경 경로, +프로세스 환경 override와 Development Live 실행 인수를 사용하지 않으며 안전한 DryRun을 +강제한다. 설정이 성공하면 창을 닫고 Visual Studio에서 F5를 한 번 더 눌러 전체 자산 +Development Live 빌드를 시작한다. 출력과 중간 패키징 파일은 이전 전체 빌드의 자산이나 +`MmoneyCoder.ini`를 재사용하지 않도록 다음 별도 경로에 생성된다. + +첫 실행 화면에서 개별 INI, DB endpoint 또는 Tornado2 port를 따로 입력하지 않는다. 선택한 +`Res`에서 `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`, +`해외.ini`, `환율.ini`, `지수.ini`, `종목비교.ini` 등 활성 INI 8개를 모두 검증하고, +Tornado2 endpoint는 지정 개발 환경의 `127.0.0.1:30001`로 고정한다. 화면을 사용할 수 없는 +장애 대응 또는 기존 로컬 설정의 명시적 교체에만 `Initialize-ExistingDevelopmentPc.ps1`을 +수동 fallback으로 사용한다. ```text src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\SourceOnly\... @@ -107,6 +119,11 @@ CP949 레거시 형식으로 읽고, 파일의 section/row/order를 기존의 %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini ``` +이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 설정은 +원본 `MmoneyCoder.ini`를 위 사용자 전용 경로에만 복사하고 현재 Windows 사용자 전용 ACL로 +보호한다. DB 자격증명은 Git, Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지 +않는다. + 사용자 설정 메뉴에서 별도 `Res` 폴더를 선택했고 그 안에 `MmoneyCoder.ini`가 있으면 앱은 저장 전에 형식을 네이티브에서 검증하고 다음 시작부터 그 DB 설정을 우선 사용한다. 파일 내용과 자격증명은 Web 화면에 노출하지 않으며 화면에서 직접 편집할 수도 없다. 단, one-shot Gate A diff --git a/docs/LEGACY_RUNTIME_ASSET_VALIDATION_20260718.md b/docs/LEGACY_RUNTIME_ASSET_VALIDATION_20260718.md index 6869333..b8202c1 100644 --- a/docs/LEGACY_RUNTIME_ASSET_VALIDATION_20260718.md +++ b/docs/LEGACY_RUNTIME_ASSET_VALIDATION_20260718.md @@ -1,4 +1,10 @@ -# Cuts/Res 실행 구조 검증 (2026-07-18) +# [과거 기록] Cuts/Res 실행 구조 검증 (2026-07-18) + +> 이 문서는 2026-07-18 당시 빌드와 패키지를 검증한 역사적 증거이며 현행 배치 계약이 아니다. +> 현재 Debug/Release 출력, 게시와 MSIX에는 `MmoneyCoder.ini`를 포함하지 않는다. 현행 앱은 +> 첫 실행에서 선택한 원본을 +> `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini` 사용자 전용 overlay로만 복사한다. +> 최신 계약은 [원본 Cuts/Res 런타임 배치](LEGACY_RUNTIME_ASSETS.md)를 따른다. ## 적용 결과 @@ -8,9 +14,10 @@ C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\bin\Debug ``` -`LegacyParityApp`은 이 위치의 `Cuts`와 허용된 `Res`를 빌드 입력으로 연결한다. 일반 -Visual Studio Debug 출력에는 실제 DB용 `Res\MmoneyCoder.ini`도 실행 파일 옆에 복사하되, -Git·게시·MSIX에는 넣지 않는다. 원본 저장소의 기존 수정 파일은 변경하지 않았다. +당시 `LegacyParityApp`은 이 위치의 `Cuts`와 허용된 `Res`를 빌드 입력으로 연결했다. 당시 +일반 Visual Studio Debug 출력에는 실제 DB용 `Res\MmoneyCoder.ini`도 실행 파일 옆에 +복사했지만, 이 동작은 이후 폐기되었다. 현행 빌드는 위 안내처럼 DB INI를 어떤 빌드 +출력에도 넣지 않는다. 원본 저장소의 기존 수정 파일은 변경하지 않았다. ## 자산 무결성 @@ -20,7 +27,7 @@ Git·게시·MSIX에는 넣지 않는다. 원본 저장소의 기존 수정 파 | Debug 실행 출력 `Cuts` | 179개, 원본과 파일별 경로·크기·SHA-256 일치 | | Release MSIX `Cuts` | 179개 / 222,825,612 bytes, 원본과 파일별 경로·크기·SHA-256 일치 | | Release MSIX `Res` | 허용 목록 34개 / 2,920,725 bytes, 원본과 파일별 크기·SHA-256 일치 | -| Debug 실행 출력 `Res` | UI용 34개 + DB INI 1개 | +| 당시 Debug 실행 출력 `Res` | UI용 34개 + DB INI 1개(현행 계약에서는 DB INI 제외) | | 필수 장면 | `Cuts\5001.t2s` 포함 | Release x64 개발 MSIX 결과는 다음과 같다. @@ -39,9 +46,10 @@ Release x64 개발 MSIX 결과는 다음과 같다. `validate` 명령이 없어 해당 부가 명령은 사용할 수 없었고, 패키지 생성·BlockMap 및 파일별 payload 감사와는 별개다. -추가 감사에서 일반 출력의 DB INI가 `Clean` 뒤 남을 가능성과 패키지 빌드가 불필요하게 -원본 DB INI를 요구하는 문제를 발견해 보완했다. 현재 일반 빌드 산출물은 `Clean`이 DB -INI를 제거하고, MSIX 생성·게시 경로는 원본 DB INI가 없어도 검증 단계를 통과한다. +당시 추가 감사에서 일반 출력의 DB INI가 `Clean` 뒤 남을 가능성과 패키지 빌드가 불필요하게 +원본 DB INI를 요구하는 문제를 발견해 보완했다. 이 문단의 “일반 빌드”는 2026-07-18 회차를 +가리킨다. 현행 일반 빌드는 DB INI를 출력에 복사하지 않으며, MSIX 생성·게시 경로도 원본 DB +INI를 포함하지 않는다. ## 런타임 검증 diff --git a/scripts/Initialize-ExistingDevelopmentPc.ps1 b/scripts/Initialize-ExistingDevelopmentPc.ps1 index fda4c54..0960687 100644 --- a/scripts/Initialize-ExistingDevelopmentPc.ps1 +++ b/scripts/Initialize-ExistingDevelopmentPc.ps1 @@ -24,6 +24,10 @@ param( [string] $InteropSha256, + [switch] $PinRegisteredK3D, + + [switch] $ReplaceK3DPin, + [ValidateRange(0, 2147483647)] [Nullable[int]] $OutputChannel = $null, @@ -1411,7 +1415,7 @@ function Get-DevelopmentLivePreflight { return [pscustomobject]@{ # Every requested setup invalidates any old one-launch authorization and - # issues a new file only after runtime/DB/build/cleanup success. + # issues a new file only after runtime/DB/build/K3D verification succeeds. NeedsWrite = $true NeedsForce = [bool] ($playoutExists -or $authorizationExists) PlayoutPath = $playoutPath @@ -1421,6 +1425,144 @@ function Get-DevelopmentLivePreflight { } } +function Get-K3DPinPreflight { + param( + [Parameter(Mandatory = $true)][string] $ConfigurationRoot, + [Parameter(Mandatory = $true)][string] $ApprovedNativeHash, + [Parameter(Mandatory = $true)][string] $ApprovedInteropHash, + [switch] $AllowReplacement + ) + + $pin = [ordered]@{ + schemaVersion = 1 + nativeSha256 = $ApprovedNativeHash.ToUpperInvariant() + interopSha256 = $ApprovedInteropHash.ToUpperInvariant() + } + $pinJson = Get-CanonicalJsonText -Value $pin -Depth 3 + $pinPath = Join-Path $ConfigurationRoot 'k3d-pins.local.json' + Assert-NoReparsePointInExistingAncestry ` + -Path $ConfigurationRoot ` + -Description 'K3D pin configuration directory' + $pinExists = Test-Path -LiteralPath $pinPath + if ($pinExists) { + [void](Assert-RegularFile ` + -Path $pinPath ` + -Description 'K3D persistent pin') + } + + $pinMatches = $pinExists -and + (Test-ExistingJsonContract -Path $pinPath -ExpectedJson $pinJson) + if ($pinExists -and -not $pinMatches -and -not $AllowReplacement) { + throw ( + 'The installed K3D files differ from the persistent local pin. ' + + 'Automatic pinning cannot approve a changed DLL. Independently ' + + 'verify the vendor files before using -ReplaceK3DPin with supplied hashes.') + } + + return [pscustomobject]@{ + NeedsWrite = [bool] (-not $pinExists -or -not $pinMatches) + NeedsReplace = [bool] $pinExists + Path = $pinPath + Json = $pinJson + } +} + +function Write-ProtectedUtf8TextAtomically { + param( + [Parameter(Mandatory = $true)][string] $Path, + [Parameter(Mandatory = $true)][string] $ExpectedText, + [Parameter(Mandatory = $true)][string] $Description, + [switch] $Replace + ) + + $destination = Get-NormalizedFullPath -Path $Path + $directory = [IO.Path]::GetDirectoryName($destination) + [IO.Directory]::CreateDirectory($directory) | Out-Null + Assert-NoReparsePointInExistingAncestry ` + -Path $directory ` + -Description "$Description directory" + Set-UserOnlyDirectoryAcl -Path $directory + + $temporaryPath = Join-Path $directory ( + '.' + [IO.Path]::GetFileName($destination) + '.' + + [Guid]::NewGuid().ToString('N') + '.tmp') + $rollbackSnapshot = New-FileRollbackSnapshot ` + -Path $destination ` + -BackupDirectory $directory ` + -Description $Description + $mutationStarted = $false + $preserveRollbackBackup = $false + try { + [IO.File]::WriteAllText( + $temporaryPath, + $ExpectedText, + (New-Object Text.UTF8Encoding($false))) + [IO.File]::SetAccessControl($temporaryPath, (Get-UserOnlyAcl)) + if (-not (Test-ExactUtf8Text ` + -Path $temporaryPath ` + -ExpectedText $ExpectedText) -or + -not (Test-UserOnlyFileAcl -Path $temporaryPath)) { + throw "$Description temporary file failed exact JSON/ACL verification." + } + + if (Test-Path -LiteralPath $destination) { + if (-not $Replace) { + throw "Refusing to replace the existing $Description." + } + [void](Assert-RegularFile -Path $destination -Description $Description) + $mutationStarted = $true + [IO.File]::Replace($temporaryPath, $destination, $null) + $temporaryPath = $null + } + else { + $mutationStarted = $true + [IO.File]::Move($temporaryPath, $destination) + $temporaryPath = $null + } + + [IO.File]::SetAccessControl($destination, (Get-UserOnlyAcl)) + if (-not (Test-ExactUtf8Text ` + -Path $destination ` + -ExpectedText $ExpectedText) -or + -not (Test-UserOnlyFileAcl -Path $destination)) { + throw "The atomically installed $Description failed exact JSON/ACL verification." + } + } + catch { + $writeError = $_ + if ($mutationStarted) { + try { + Restore-FileRollbackSnapshot ` + -Snapshot $rollbackSnapshot ` + -Description $Description + } + catch { + $preserveRollbackBackup = $true + throw [InvalidOperationException]::new( + ( + "$Description write failed and its previous trust " + + 'anchor could not be restored: ' + $_.Exception.Message + ), + $writeError.Exception) + } + } + throw $writeError + } + finally { + if (-not [string]::IsNullOrWhiteSpace($temporaryPath) -and + (Test-Path -LiteralPath $temporaryPath)) { + [IO.File]::Delete($temporaryPath) + } + if (-not $preserveRollbackBackup -and + $null -ne $rollbackSnapshot -and + -not [string]::IsNullOrWhiteSpace( + [string] $rollbackSnapshot.BackupPath) -and + (Test-Path -LiteralPath ([string] $rollbackSnapshot.BackupPath))) { + [IO.File]::Delete([string] $rollbackSnapshot.BackupPath) + } + } +} + function Copy-DatabaseProfileAtomically { param( [Parameter(Mandatory = $true)][string] $Source, @@ -1624,21 +1766,29 @@ function Assert-DebugRuntimePayloadMatchesManifest { function Invoke-DebugBuildAndVerify { param( + [Parameter(Mandatory = $true)][string] $DotNetPath, [Parameter(Mandatory = $true)][string] $ProjectPath, [Parameter(Mandatory = $true)][string] $ExpectedRuntimeRoot, [Parameter(Mandatory = $true)][string] $ExpectedManifestPath, [Parameter(Mandatory = $true)][string] $ExpectedManifestSha256 ) - & dotnet build $ProjectPath -c Debug '-p:Platform=x64' | + $runtimeRootProperty = "-p:LegacyRuntimeSourceRoot=$ExpectedRuntimeRoot" + & $DotNetPath build $ProjectPath ` + -c Debug ` + '-p:Platform=x64' ` + '-p:LegacyRuntimeAssetsMode=Required' ` + $runtimeRootProperty | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { throw "Debug x64 build failed with exit code $LASTEXITCODE." } - $propertyText = (& dotnet msbuild $ProjectPath ` + $propertyText = (& $DotNetPath msbuild $ProjectPath ` '-p:Configuration=Debug' ` '-p:Platform=x64' ` + '-p:LegacyRuntimeAssetsMode=Required' ` + $runtimeRootProperty ` '-getProperty:TargetDir' ` '-getProperty:LegacyRuntimeAssetsEnabled' ` '-getProperty:LegacyRuntimeAssetsMode' ` @@ -1710,6 +1860,23 @@ function Invoke-DebugBuildAndVerify { } } +$setupMutex = [Threading.Mutex]::new( + $false, + 'Local\MBN_STOCK_WEBVIEW.ExistingDevelopmentPcInitialization.v1') +$setupMutexOwned = $false +try { +try { + $setupMutexOwned = $setupMutex.WaitOne(0) +} +catch [Threading.AbandonedMutexException] { + # The prior process ended unexpectedly. Atomic local files are re-preflighted + # below so this run can safely complete the setup forward from that state. + $setupMutexOwned = $true +} +if (-not $setupMutexOwned) { + throw 'Another existing-development-PC initialization is already running.' +} + $repositoryRoot = Get-NormalizedFullPath -Path (Join-Path $PSScriptRoot '..') $projectPath = Join-Path $repositoryRoot ( 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj') @@ -1729,6 +1896,14 @@ foreach ($requiredScript in @( if ($ReplaceLiveConfig -and -not $ConfigureDevelopmentLive) { throw '-ReplaceLiveConfig requires -ConfigureDevelopmentLive.' } +if ($ReplaceK3DPin -and -not $ConfigureDevelopmentLive) { + throw '-ReplaceK3DPin requires -ConfigureDevelopmentLive.' +} +if ($ReplaceK3DPin -and $PinRegisteredK3D) { + throw ( + '-ReplaceK3DPin cannot be combined with -PinRegisteredK3D. ' + + 'A replacement requires independently supplied native and interop hashes.') +} if ($ConfigureDevelopmentLive -and $SkipBuild) { throw '-ConfigureDevelopmentLive requires the default Debug x64 build verification.' } @@ -1737,10 +1912,25 @@ if (-not $ConfigureDevelopmentLive -and $PlayoutPort -ne 0 -or -not [string]::IsNullOrWhiteSpace($NativeSha256) -or -not [string]::IsNullOrWhiteSpace($InteropSha256) -or + $PinRegisteredK3D -or + $ReplaceK3DPin -or $null -ne $OutputChannel)) { throw 'Development Live endpoint/hash options require -ConfigureDevelopmentLive.' } +$dotnetPath = $null +if (-not $SkipBuild) { + $dotnetCommand = Get-Command -Name 'dotnet.exe' -CommandType Application ` + -ErrorAction Stop | Select-Object -First 1 + if ($null -eq $dotnetCommand -or + [string]::IsNullOrWhiteSpace([string] $dotnetCommand.Source)) { + throw 'A .NET SDK dotnet.exe could not be resolved for build verification.' + } + $dotnetPath = Get-NormalizedFullPath -Path ([string] $dotnetCommand.Source) + [void](Assert-RegularFile -Path $dotnetPath -Description '.NET SDK host') + Assert-FixedLocalPath -Path $dotnetPath -Description '.NET SDK host' +} + $runtimeSourceRoot = Select-LegacyRuntimeRoot ` -RepositoryRoot $repositoryRoot ` -RequestedRoot $LegacyRuntimeSourceRoot ` @@ -1792,9 +1982,16 @@ Assert-NoReparsePointInExistingAncestry ` -Description 'LocalAppData' # Preflight a requested Live replacement before any setup write. The caller -# supplies both approval hashes; this script only verifies them against files. +# either supplies both independent approval hashes or explicitly requests +# first-use pinning of the already verified HKLM x64 K3D registration. Both +# paths keep the exact file hashes in the one-launch authorization and the app +# verifies them again before loading either vendor binary. $livePreflight = $null +$k3dPinPreflight = $null +$k3dPinWritten = $false $liveAuthorizationInvalidated = $false +$approvedNativeSha256 = $null +$approvedInteropSha256 = $null if ($ConfigureDevelopmentLive) { $configurationRoot = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\Config' $prospectiveAuthorizationPath = Join-Path $configurationRoot ( @@ -1806,19 +2003,67 @@ if ($ConfigureDevelopmentLive) { if ($PlayoutPort -lt 1 -or $PlayoutPort -gt 65535) { throw 'Development Live playout port must be between 1 and 65535.' } - if ([string]::IsNullOrWhiteSpace($NativeSha256) -or - $NativeSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or - [string]::IsNullOrWhiteSpace($InteropSha256) -or - $InteropSha256 -notmatch '^[0-9A-Fa-f]{64}$') { - throw 'Independent native and interop SHA-256 approvals are both required.' + + $actualNativeHash = ( + Get-FileHash -LiteralPath $nativePath -Algorithm SHA256 + ).Hash.ToUpperInvariant() + $actualInteropHash = ( + Get-FileHash -LiteralPath $interopPath -Algorithm SHA256 + ).Hash.ToUpperInvariant() + if ($PinRegisteredK3D) { + if (-not [string]::IsNullOrWhiteSpace($NativeSha256) -or + -not [string]::IsNullOrWhiteSpace($InteropSha256)) { + throw ( + '-PinRegisteredK3D cannot be combined with independently ' + + 'supplied K3D hashes.') + } + + $approvedNativeSha256 = $actualNativeHash + $approvedInteropSha256 = $actualInteropHash + } + else { + if ([string]::IsNullOrWhiteSpace($NativeSha256) -or + $NativeSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or + [string]::IsNullOrWhiteSpace($InteropSha256) -or + $InteropSha256 -notmatch '^[0-9A-Fa-f]{64}$') { + throw 'Independent native and interop SHA-256 approvals are both required.' + } + + $approvedNativeSha256 = $NativeSha256.ToUpperInvariant() + $approvedInteropSha256 = $InteropSha256.ToUpperInvariant() + if (-not $actualNativeHash.Equals( + $approvedNativeSha256, + [StringComparison]::Ordinal) -or + -not $actualInteropHash.Equals( + $approvedInteropSha256, + [StringComparison]::Ordinal)) { + throw 'An independently supplied K3D SHA-256 does not match the registered files.' + } } + $k3dPinPreflight = Get-K3DPinPreflight ` + -ConfigurationRoot $configurationRoot ` + -ApprovedNativeHash $approvedNativeSha256 ` + -ApprovedInteropHash $approvedInteropSha256 ` + -AllowReplacement:$ReplaceK3DPin + if ($k3dPinPreflight.NeedsWrite) { + # This is a durable trust anchor, not transactional runtime state. + # Preserve the first-seen/independently replaced pin even when a + # later DB, build, Live-config, or cleanup step fails. Otherwise a + # changed DLL could be automatically accepted on the next attempt. + Write-ProtectedUtf8TextAtomically ` + -Path ([string] $k3dPinPreflight.Path) ` + -ExpectedText ([string] $k3dPinPreflight.Json) ` + -Description 'K3D persistent pin' ` + -Replace:$k3dPinPreflight.NeedsReplace + $k3dPinWritten = $true + } $livePreflight = Get-DevelopmentLivePreflight ` -ConfigurationRoot $configurationRoot ` -HostName $PlayoutHost ` -Port $PlayoutPort ` - -ApprovedNativeHash $NativeSha256 ` - -ApprovedInteropHash $InteropSha256 ` + -ApprovedNativeHash $approvedNativeSha256 ` + -ApprovedInteropHash $approvedInteropSha256 ` -Channel $OutputChannel ` -AllowReplacement:$ReplaceLiveConfig } @@ -1849,20 +2094,6 @@ if ($ConfigureDevelopmentLive) { } $liveAuthorizationInvalidated = $true - $actualNativeHash = ( - Get-FileHash -LiteralPath $nativePath -Algorithm SHA256 - ).Hash.ToUpperInvariant() - $actualInteropHash = ( - Get-FileHash -LiteralPath $interopPath -Algorithm SHA256 - ).Hash.ToUpperInvariant() - if (-not $actualNativeHash.Equals( - $NativeSha256, - [StringComparison]::OrdinalIgnoreCase) -or - -not $actualInteropHash.Equals( - $InteropSha256, - [StringComparison]::OrdinalIgnoreCase)) { - throw 'An independently supplied K3D SHA-256 does not match the registered files.' - } } $databaseSource = $null @@ -1949,6 +2180,7 @@ $setupRoot = Join-Path $setupParent ([Guid]::NewGuid().ToString('N')) $rollbackRoot = Join-Path $rollbackParent ([Guid]::NewGuid().ToString('N')) $runtimeResult = $null +$pendingRuntimeBinding = $null $buildResult = $null $databaseInstalled = $false $liveConfigured = $false @@ -1956,6 +2188,8 @@ $runtimeBindingMutationStarted = $false $databaseMutationStarted = $false $propsSnapshot = $null $databaseSnapshot = $null +$playoutSnapshot = $null +$playoutMutationStarted = $false $transactionCommitted = $false $transactionError = $null try { @@ -1988,6 +2222,12 @@ try { -BackupDirectory $rollbackRoot ` -Description 'Installed database profile' } + if ($ConfigureDevelopmentLive) { + $playoutSnapshot = New-FileRollbackSnapshot ` + -Path ([string] $livePreflight.PlayoutPath) ` + -BackupDirectory $rollbackRoot ` + -Description 'Development Live base configuration' + } $bundleOutput = Join-Path $setupRoot 'bundle' $bundleResults = @(& $bundleCreatorPath ` @@ -2061,35 +2301,35 @@ try { -ShadowRepositoryRoot $shadowRepositoryRoot ` -RuntimeStorageRoot $runtimeStorageRoot - $runtimeBindingMutationStarted = $true - Set-RuntimeBindingAtomically ` - -GeneratedPropsPath $initialized.LocalBuildPropsPath ` - -RepositoryRoot $repositoryRoot ` - -Replace:($null -ne $existingRuntime) - $verifiedBinding = Get-ExistingRuntimeBinding ` - -RepositoryRoot $repositoryRoot ` - -RuntimeStorageRoot $runtimeStorageRoot - if ($null -eq $verifiedBinding -or - -not (Test-SamePath ` - -Left $verifiedBinding.InstalledRoot ` - -Right ([string] $initialized.InstalledRoot)) -or - -not $verifiedBinding.ManifestSha256.Equals( - [string] $initialized.ManifestSha256, - [StringComparison]::Ordinal)) { - throw 'The atomically installed runtime binding failed final verification.' - } - Assert-InstalledRuntimeMatchesManifest ` - -InstalledRoot $verifiedBinding.InstalledRoot ` - -ManifestPath $newManifestPath - $runtimeResult = [pscustomobject]@{ - InstalledRoot = $verifiedBinding.InstalledRoot - ManifestSha256 = $verifiedBinding.ManifestSha256 - ManifestPath = $verifiedBinding.ManifestPath + InstalledRoot = [string] $initialized.InstalledRoot + ManifestSha256 = [string] $initialized.ManifestSha256 + ManifestPath = Join-Path ` + ([string] $initialized.InstalledRoot) ` + $manifestFileName CutsFileCount = [int] $initialized.CutsFileCount ResFileCount = [int] $initialized.ResFileCount Reused = $false } + $pendingRuntimeBinding = [pscustomobject]@{ + GeneratedPropsPath = [string] $initialized.LocalBuildPropsPath + Replace = [bool] ($null -ne $existingRuntime) + } + } + + if (-not $SkipBuild) { + $buildResult = Invoke-DebugBuildAndVerify ` + -DotNetPath $dotnetPath ` + -ProjectPath $projectPath ` + -ExpectedRuntimeRoot $runtimeResult.InstalledRoot ` + -ExpectedManifestPath $runtimeResult.ManifestPath ` + -ExpectedManifestSha256 $runtimeResult.ManifestSha256 + if ([int] $buildResult.CutsFileCount -ne + [int] $runtimeResult.CutsFileCount -or + [int] $buildResult.ResFileCount -ne + [int] $runtimeResult.ResFileCount) { + throw 'Debug x64 payload count mismatch after exact manifest verification.' + } } if ($databaseNeedsWrite) { @@ -2102,22 +2342,92 @@ try { $databaseInstalled = $true } - if (-not $SkipBuild) { - $buildResult = Invoke-DebugBuildAndVerify ` - -ProjectPath $projectPath ` - -ExpectedRuntimeRoot $runtimeResult.InstalledRoot ` - -ExpectedManifestPath $runtimeResult.ManifestPath ` - -ExpectedManifestSha256 $runtimeResult.ManifestSha256 - if ([int] $buildResult.CutsFileCount -ne - [int] $runtimeResult.CutsFileCount -or - [int] $buildResult.ResFileCount -ne - [int] $runtimeResult.ResFileCount) { - throw 'Debug x64 payload count mismatch after exact manifest verification.' + if ($ConfigureDevelopmentLive) { + [void](Assert-RegularFile ` + -Path $nativePath ` + -Description 'Registered K3D native DLL before authorization') + [void](Assert-RegularFile ` + -Path $interopPath ` + -Description 'K3D interop assembly before authorization') + $finalNativeHash = ( + Get-FileHash -LiteralPath $nativePath -Algorithm SHA256 + ).Hash.ToUpperInvariant() + $finalInteropHash = ( + Get-FileHash -LiteralPath $interopPath -Algorithm SHA256 + ).Hash.ToUpperInvariant() + if (-not $finalNativeHash.Equals( + $approvedNativeSha256, + [StringComparison]::Ordinal) -or + -not $finalInteropHash.Equals( + $approvedInteropSha256, + [StringComparison]::Ordinal)) { + throw ( + 'A K3D DLL changed during setup. The durable pin remains at ' + + 'the originally approved hash, and no Live authorization was issued.') } } - # Both working data and rollback copies must be gone before a new Live - # authorization can be issued. A cleanup failure is a transaction failure. + # Live authorization is the final functional mutation. No COM activation, + # PGM command, or database connection is performed by this setup script. + if ($ConfigureDevelopmentLive -and $livePreflight.NeedsWrite) { + $liveArguments = @{ + PlayoutHost = $PlayoutHost + PlayoutPort = $PlayoutPort + NativeSha256 = $approvedNativeSha256 + InteropSha256 = $approvedInteropSha256 + ConfigurationDirectory = $configurationRoot + } + if ($null -ne $OutputChannel) { + $liveArguments.OutputChannel = $OutputChannel + } + if ($livePreflight.NeedsForce) { + $liveArguments.Force = $true + } + + $playoutMutationStarted = $true + $liveResults = @(& $liveInitializerPath @liveArguments) + if ($liveResults.Count -ne 0) { + throw 'Development Live initializer returned unexpected success-stream output.' + } + Assert-ExistingJsonContract ` + -Path ([string] $livePreflight.PlayoutPath) ` + -ExpectedJson ([string] $livePreflight.PlayoutJson) ` + -Description 'Development Live base configuration' + Assert-ExistingJsonContract ` + -Path ([string] $livePreflight.AuthorizationPath) ` + -ExpectedJson ([string] $livePreflight.AuthorizationJson) ` + -Description 'Development Live authorization' + $liveConfigured = $true + } + + # Directory.Build.local.props is the durable success marker for a fresh + # clone. Keep it source-only until runtime/build/DB/K3D/Live are complete so + # a stopped setup returns to this first-run UI on the next F5. + if ($null -ne $pendingRuntimeBinding) { + $runtimeBindingMutationStarted = $true + Set-RuntimeBindingAtomically ` + -GeneratedPropsPath ([string] $pendingRuntimeBinding.GeneratedPropsPath) ` + -RepositoryRoot $repositoryRoot ` + -Replace:([bool] $pendingRuntimeBinding.Replace) + $verifiedBinding = Get-ExistingRuntimeBinding ` + -RepositoryRoot $repositoryRoot ` + -RuntimeStorageRoot $runtimeStorageRoot + if ($null -eq $verifiedBinding -or + -not (Test-SamePath ` + -Left $verifiedBinding.InstalledRoot ` + -Right ([string] $runtimeResult.InstalledRoot)) -or + -not $verifiedBinding.ManifestSha256.Equals( + [string] $runtimeResult.ManifestSha256, + [StringComparison]::Ordinal)) { + throw 'The atomically installed runtime binding failed final verification.' + } + Assert-InstalledRuntimeMatchesManifest ` + -InstalledRoot $verifiedBinding.InstalledRoot ` + -ManifestPath $runtimeResult.ManifestPath + } + + # A cleanup failure is still a transaction failure. Rollback snapshots are + # also held in memory so rollback remains possible if cleanup only partly ran. Remove-OwnedTemporaryDirectory ` -Parent $setupParent ` -Path $setupRoot ` @@ -2135,6 +2445,30 @@ catch { if (-not $transactionCommitted) { $secondaryFailures = New-Object 'System.Collections.Generic.List[string]' + # Always fail closed before restoring any build/runtime state. + if ($ConfigureDevelopmentLive -and $liveAuthorizationInvalidated) { + try { + Remove-DevelopmentLiveAuthorizationFailClosed ` + -Path ([string] $livePreflight.AuthorizationPath) + } + catch { + $secondaryFailures.Add( + 'Development Live fail-closed cleanup failed: ' + + $_.Exception.Message) + } + } + if ($playoutMutationStarted -and $null -ne $playoutSnapshot) { + try { + Restore-FileRollbackSnapshot ` + -Snapshot $playoutSnapshot ` + -Description 'Development Live base configuration' + } + catch { + $secondaryFailures.Add( + 'Development Live base configuration rollback failed: ' + + $_.Exception.Message) + } + } if ($databaseMutationStarted -and $null -ne $databaseSnapshot) { try { Restore-FileRollbackSnapshot ` @@ -2182,18 +2516,6 @@ if (-not $transactionCommitted) { } } - if ($ConfigureDevelopmentLive -and $liveAuthorizationInvalidated) { - try { - Remove-DevelopmentLiveAuthorizationFailClosed ` - -Path ([string] $livePreflight.AuthorizationPath) - } - catch { - $secondaryFailures.Add( - 'Development Live fail-closed cleanup failed: ' + - $_.Exception.Message) - } - } - if ($null -eq $transactionError) { $transactionError = [System.Management.Automation.ErrorRecord]::new( [InvalidOperationException]::new( @@ -2214,56 +2536,6 @@ if (-not $transactionCommitted) { throw $transactionError } -# Live authorization is the final mutation, after build verification and after -# temporary cleanup. No COM activation, PGM command, or database connection is -# performed by this setup script. -if ($ConfigureDevelopmentLive -and $livePreflight.NeedsWrite) { - $liveArguments = @{ - PlayoutHost = $PlayoutHost - PlayoutPort = $PlayoutPort - NativeSha256 = $NativeSha256 - InteropSha256 = $InteropSha256 - ConfigurationDirectory = $configurationRoot - } - if ($null -ne $OutputChannel) { - $liveArguments.OutputChannel = $OutputChannel - } - if ($livePreflight.NeedsForce) { - $liveArguments.Force = $true - } - try { - $liveResults = @(& $liveInitializerPath @liveArguments) - if ($liveResults.Count -ne 0) { - throw 'Development Live initializer returned unexpected success-stream output.' - } - Assert-ExistingJsonContract ` - -Path ([string] $livePreflight.PlayoutPath) ` - -ExpectedJson ([string] $livePreflight.PlayoutJson) ` - -Description 'Development Live base configuration' - Assert-ExistingJsonContract ` - -Path ([string] $livePreflight.AuthorizationPath) ` - -ExpectedJson ([string] $livePreflight.AuthorizationJson) ` - -Description 'Development Live authorization' - $liveConfigured = $true - } - catch { - $liveError = $_ - try { - Remove-DevelopmentLiveAuthorizationFailClosed ` - -Path ([string] $livePreflight.AuthorizationPath) - } - catch { - throw [InvalidOperationException]::new( - ( - 'Development Live configuration failed and fail-closed ' + - 'authorization cleanup also failed: ' + $_.Exception.Message - ), - $liveError.Exception) - } - throw $liveError - } -} - [pscustomobject]@{ RuntimeSourceRoot = $runtimeSourceRoot InstalledRuntimeRoot = $runtimeResult.InstalledRoot @@ -2275,6 +2547,14 @@ if ($ConfigureDevelopmentLive -and $livePreflight.NeedsWrite) { DatabaseProfileWritten = $databaseInstalled K3DStatus = [string] $registration.Status K3DComActivated = [bool] $registration.ComActivated + K3DPinPath = if ($ConfigureDevelopmentLive) { + [string] $k3dPinPreflight.Path + } else { + $null + } + K3DPinWritten = $k3dPinWritten + K3DPinReused = [bool] ( + $ConfigureDevelopmentLive -and -not $k3dPinPreflight.NeedsWrite) BuildVerified = $null -ne $buildResult BuildOutputPath = if ($null -eq $buildResult) { $null } else { $buildResult.TargetDirectory @@ -2283,3 +2563,14 @@ if ($ConfigureDevelopmentLive -and $livePreflight.NeedsWrite) { DevelopmentLiveWritten = $liveConfigured DevelopmentLiveReused = $false } +} +finally { + try { + if ($setupMutexOwned) { + $setupMutex.ReleaseMutex() + } + } + finally { + $setupMutex.Dispose() + } +} diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs index aab7cb4..5d41157 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs @@ -70,7 +70,13 @@ public partial class App : Application System.Diagnostics.Debug.WriteLine( $"Development Live bootstrap: {developmentLive.Code}"); - _window = new MainWindow(); + // A fresh clone cannot become a full runtime inside the already compiled + // source-only process. Show only the native two-folder setup window; the + // reviewed initializer builds the verified runtime for the next F5. + var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild; + _window = showFirstRunSetup + ? new FirstRunSetupWindow() + : new MainWindow(); _window.Activate(); } diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml new file mode 100644 index 0000000..f56499f --- /dev/null +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + +