feat: add two-folder first-run playout setup
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
102
README.md
102
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 '<Cuts와 Res의 공통 부모>' `
|
||||
-ConfigureDevelopmentLive `
|
||||
-PlayoutHost 127.0.0.1 `
|
||||
-PlayoutPort <Tornado2에서 직접 확인한 Network-Server-port> `
|
||||
-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 절차를
|
||||
|
||||
@@ -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 '<Cuts와 Res의 공통 부모>' `
|
||||
-ConfigureDevelopmentLive `
|
||||
-PlayoutHost 127.0.0.1 `
|
||||
-PlayoutPort <Tornado2에서 직접 확인한 Network-Server-port> `
|
||||
-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 <verified-channel>`로 추가하고, 기본 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\<ZIP-SHA256>`에 검증된 복사본을 설치한다.
|
||||
- 저장소에서 제외되는 `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 '<Cuts와 Res의 공통 부모>' `
|
||||
-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 '<Cuts와 Res의 공통 부모>' `
|
||||
-NoFolderPicker `
|
||||
-ConfigureDevelopmentLive `
|
||||
-PlayoutHost 127.0.0.1 `
|
||||
-PlayoutPort 30001 `
|
||||
-NativeSha256 '<independently-verified-64-hex-native-sha256>' `
|
||||
-InteropSha256 '<independently-verified-64-hex-interop-sha256>' `
|
||||
-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 <verified-network-server-tcp-port> `
|
||||
-PlayoutPort 30001 `
|
||||
-NativeSha256 '<independently-approved-64-hex-native-sha256>' `
|
||||
-InteropSha256 '<independently-approved-64-hex-interop-sha256>'
|
||||
```
|
||||
@@ -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 <verified-network-server-tcp-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 서명 인증서, 개인 키와 암호
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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를 포함하지 않는다.
|
||||
|
||||
## 런타임 검증
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
123
src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
Normal file
123
src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
Normal file
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="MBN_STOCK_WEBVIEW.LegacyParityApp.FirstRunSetupWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="V-STOCK 첫 실행 설정">
|
||||
|
||||
<Grid Background="#FF08182B">
|
||||
<ScrollViewer>
|
||||
<StackPanel
|
||||
Width="720"
|
||||
Margin="36"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="20">
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
FontFamily="Segoe UI Variable Display"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="첫 실행 설정" />
|
||||
<TextBlock
|
||||
FontSize="15"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="기존 코더의 Cuts 폴더와 여러 INI가 들어 있는 Res 폴더만 선택하면 됩니다."
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="#FF102640"
|
||||
BorderBrush="#334D79A7"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="1. Cuts 폴더" />
|
||||
<TextBlock
|
||||
x:Name="CutsPathText"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="선택되지 않음"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="ChooseCutsButton"
|
||||
HorizontalAlignment="Left"
|
||||
Click="OnChooseCutsClicked"
|
||||
Content="Cuts 폴더 선택" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="#FF102640"
|
||||
BorderBrush="#334D79A7"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="2. 설정/INI 폴더 (Res)" />
|
||||
<TextBlock
|
||||
Foreground="#FF8FAAC5"
|
||||
Text="MmoneyCoder.ini와 종목·업종·해외·환율·지수·종목비교 INI를 함께 확인합니다."
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Name="ResourcePathText"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="선택되지 않음"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="ChooseResourceButton"
|
||||
HorizontalAlignment="Left"
|
||||
Click="OnChooseResourceClicked"
|
||||
Content="Res 폴더 선택" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<InfoBar
|
||||
x:Name="SetupStatusBar"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="두 폴더를 선택해 주세요."
|
||||
Severity="Informational"
|
||||
Title="준비" />
|
||||
|
||||
<ProgressBar
|
||||
x:Name="SetupProgress"
|
||||
Height="4"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<Button
|
||||
x:Name="CloseButton"
|
||||
Click="OnCloseClicked"
|
||||
Content="닫기"
|
||||
Visibility="Collapsed" />
|
||||
<Button
|
||||
x:Name="InstallButton"
|
||||
Click="OnInstallClicked"
|
||||
Content="설정 시작"
|
||||
IsEnabled="False"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Foreground="#FF8FAAC5"
|
||||
FontSize="13"
|
||||
Text="설정 중에는 DB나 Tornado2에 연결하지 않습니다. 완료 후 Visual Studio에서 F5를 한 번 더 누르면 실제 개발 송출 모드로 시작됩니다."
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,551 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Windows.Graphics;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
|
||||
|
||||
public sealed partial class FirstRunSetupWindow : Window
|
||||
{
|
||||
private const string RepositoryRootMetadataKey = "SourceRepositoryRoot";
|
||||
private const string SetupScriptRelativePath =
|
||||
@"scripts\Initialize-ExistingDevelopmentPc.ps1";
|
||||
private const string DefaultPlayoutHost = "127.0.0.1";
|
||||
private const int DefaultPlayoutPort = 30001;
|
||||
private const int MaximumDiagnosticCharacters = 32 * 1024;
|
||||
|
||||
private string? _cutsDirectory;
|
||||
private string? _resourceDirectory;
|
||||
private bool _setupRunning;
|
||||
private readonly AppWindow _appWindow;
|
||||
|
||||
public FirstRunSetupWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_appWindow = ResolveAppWindow();
|
||||
_appWindow.Resize(new SizeInt32(820, 720));
|
||||
_appWindow.Closing += OnAppWindowClosing;
|
||||
}
|
||||
|
||||
private async void OnChooseCutsClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectedPath = await PickFolderAsync();
|
||||
if (selectedPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cutsDirectory = null;
|
||||
CutsPathText.Text = "선택되지 않음";
|
||||
InstallButton.IsEnabled = false;
|
||||
var validation = LegacyOperatorFolderValidator.Validate(
|
||||
LegacyOperatorSettingsFolderKind.Scene,
|
||||
selectedPath);
|
||||
if (!validation.IsValid || validation.Folder is null)
|
||||
{
|
||||
ShowWarning(
|
||||
validation.WarningMessage ??
|
||||
"선택한 Cuts 폴더를 검증할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_cutsDirectory = validation.Folder.CanonicalPath;
|
||||
CutsPathText.Text = _cutsDirectory;
|
||||
RefreshReadyState();
|
||||
}
|
||||
|
||||
private async void OnChooseResourceClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectedPath = await PickFolderAsync();
|
||||
if (selectedPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_resourceDirectory = null;
|
||||
ResourcePathText.Text = "선택되지 않음";
|
||||
InstallButton.IsEnabled = false;
|
||||
var validation = LegacyOperatorFolderValidator.Validate(
|
||||
LegacyOperatorSettingsFolderKind.Resource,
|
||||
selectedPath);
|
||||
if (!validation.IsValid || validation.Folder is null)
|
||||
{
|
||||
ShowWarning(
|
||||
validation.WarningMessage ??
|
||||
"선택한 Res 폴더를 검증할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validation.Folder.DatabaseIniDetected ||
|
||||
!CanReadDatabaseIni(validation.Folder.CanonicalPath))
|
||||
{
|
||||
ShowWarning(
|
||||
"선택한 Res 폴더의 MmoneyCoder.ini를 검증할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_resourceDirectory = validation.Folder.CanonicalPath;
|
||||
ResourcePathText.Text = _resourceDirectory;
|
||||
RefreshReadyState();
|
||||
}
|
||||
|
||||
private async void OnInstallClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_setupRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryResolveRuntimeRoot(
|
||||
_cutsDirectory,
|
||||
_resourceDirectory,
|
||||
out var runtimeRoot,
|
||||
out var warningMessage))
|
||||
{
|
||||
ShowWarning(warningMessage ?? "두 폴더를 다시 확인해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
string repositoryRoot;
|
||||
try
|
||||
{
|
||||
repositoryRoot = ResolveRepositoryRoot();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowError(
|
||||
"현재 Git 소스 위치를 확인할 수 없습니다. Visual Studio에서 이 솔루션을 다시 빌드해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetSetupRunning(true);
|
||||
SetupStatusBar.Title = "설정 중";
|
||||
SetupStatusBar.Message =
|
||||
"자산·DB 설정·K3D를 검증하고 있습니다. 완료될 때까지 Visual Studio 실행을 중지하지 마세요.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Informational;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await RunSetupAsync(
|
||||
repositoryRoot,
|
||||
runtimeRoot!,
|
||||
_resourceDirectory!,
|
||||
replaceExistingLocalConfiguration: false);
|
||||
if (!result.Succeeded &&
|
||||
result.LocalConfigurationReplacementRequired)
|
||||
{
|
||||
var replaceConfirmed =
|
||||
await ConfirmLocalConfigurationReplacementAsync();
|
||||
if (!replaceConfirmed)
|
||||
{
|
||||
ShowWarning(
|
||||
"기존 로컬 설정은 변경하지 않았습니다. 다시 진행하려면 설정 시작을 눌러 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetupStatusBar.Title = "기존 설정 업데이트 중";
|
||||
SetupStatusBar.Message =
|
||||
"선택한 Cuts/Res와 기본 Tornado2 설정으로 이 앱의 로컬 설정을 갱신하고 있습니다.";
|
||||
result = await RunSetupAsync(
|
||||
repositoryRoot,
|
||||
runtimeRoot!,
|
||||
_resourceDirectory!,
|
||||
replaceExistingLocalConfiguration: true);
|
||||
}
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
ShowError(
|
||||
string.IsNullOrWhiteSpace(result.Diagnostic)
|
||||
? "첫 실행 설정에 실패했습니다. 선택한 폴더와 K3D 설치 상태를 확인해 주세요."
|
||||
: $"첫 실행 설정에 실패했습니다. {result.Diagnostic}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetupStatusBar.Title = "설정 완료";
|
||||
SetupStatusBar.Message =
|
||||
"이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요. Development Live로 실행됩니다.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Success;
|
||||
InstallButton.Visibility = Visibility.Collapsed;
|
||||
CloseButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowError(
|
||||
"첫 실행 설정을 시작하지 못했습니다. Visual Studio와 Windows PowerShell 설치 상태를 확인해 주세요.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetSetupRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseClicked(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnAppWindowClosing(
|
||||
AppWindow sender,
|
||||
AppWindowClosingEventArgs args)
|
||||
{
|
||||
if (!_setupRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Cancel = true;
|
||||
ShowWarning("설정이 끝날 때까지 창을 닫지 마세요.");
|
||||
}
|
||||
|
||||
private async Task<string?> PickFolderAsync()
|
||||
{
|
||||
if (_setupRunning)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var picker = new FolderPicker
|
||||
{
|
||||
SuggestedStartLocation = PickerLocationId.ComputerFolder,
|
||||
ViewMode = PickerViewMode.List
|
||||
};
|
||||
picker.FileTypeFilter.Add("*");
|
||||
InitializeWithWindow.Initialize(
|
||||
picker,
|
||||
WindowNative.GetWindowHandle(this));
|
||||
var selected = await picker.PickSingleFolderAsync();
|
||||
return selected?.Path;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowError("폴더 선택 창을 열 수 없습니다.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshReadyState()
|
||||
{
|
||||
if (_cutsDirectory is null || _resourceDirectory is null)
|
||||
{
|
||||
InstallButton.IsEnabled = false;
|
||||
SetupStatusBar.Title = "준비";
|
||||
SetupStatusBar.Message = "두 폴더를 모두 선택해 주세요.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Informational;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryResolveRuntimeRoot(
|
||||
_cutsDirectory,
|
||||
_resourceDirectory,
|
||||
out _,
|
||||
out var warningMessage))
|
||||
{
|
||||
InstallButton.IsEnabled = false;
|
||||
ShowWarning(warningMessage ?? "두 폴더를 다시 확인해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
InstallButton.IsEnabled = true;
|
||||
SetupStatusBar.Title = "준비 완료";
|
||||
SetupStatusBar.Message =
|
||||
"설정 시작을 누르면 검증된 파일만 이 PC의 로컬 실행 영역에 구성합니다.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Success;
|
||||
}
|
||||
|
||||
private static bool TryResolveRuntimeRoot(
|
||||
string? cutsDirectory,
|
||||
string? resourceDirectory,
|
||||
out string? runtimeRoot,
|
||||
out string? warningMessage)
|
||||
{
|
||||
runtimeRoot = null;
|
||||
warningMessage = null;
|
||||
if (string.IsNullOrWhiteSpace(cutsDirectory) ||
|
||||
string.IsNullOrWhiteSpace(resourceDirectory))
|
||||
{
|
||||
warningMessage = "Cuts 폴더와 Res 폴더를 모두 선택해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var cuts = Path.TrimEndingDirectorySeparator(Path.GetFullPath(cutsDirectory));
|
||||
var resource = Path.TrimEndingDirectorySeparator(
|
||||
Path.GetFullPath(resourceDirectory));
|
||||
if (!string.Equals(
|
||||
Path.GetFileName(cuts),
|
||||
"Cuts",
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(
|
||||
Path.GetFileName(resource),
|
||||
"Res",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
warningMessage =
|
||||
"폴더 이름이 각각 Cuts와 Res인 실제 실행 자산 폴더를 선택해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var cutsParent = Path.GetDirectoryName(cuts);
|
||||
var resourceParent = Path.GetDirectoryName(resource);
|
||||
if (string.IsNullOrWhiteSpace(cutsParent) ||
|
||||
!string.Equals(
|
||||
cutsParent,
|
||||
resourceParent,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
warningMessage =
|
||||
"같은 위치에 나란히 있는 Cuts 폴더와 Res 폴더를 선택해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeRoot = cutsParent;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanReadDatabaseIni(string resourceDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
|
||||
Path.Combine(resourceDirectory, "MmoneyCoder.ini"));
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveRepositoryRoot()
|
||||
{
|
||||
var metadata = typeof(FirstRunSetupWindow).Assembly
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.Single(attribute => string.Equals(
|
||||
attribute.Key,
|
||||
RepositoryRootMetadataKey,
|
||||
StringComparison.Ordinal));
|
||||
if (string.IsNullOrWhiteSpace(metadata.Value))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The source repository metadata is empty.");
|
||||
}
|
||||
|
||||
var repositoryRoot = Path.TrimEndingDirectorySeparator(
|
||||
Path.GetFullPath(metadata.Value));
|
||||
var setupScript = Path.Combine(
|
||||
repositoryRoot,
|
||||
SetupScriptRelativePath);
|
||||
var project = Path.Combine(
|
||||
repositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp.csproj");
|
||||
if (!File.Exists(setupScript) || !File.Exists(project))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The source repository metadata is not usable.");
|
||||
}
|
||||
|
||||
return repositoryRoot;
|
||||
}
|
||||
|
||||
private static async Task<FirstRunSetupResult> RunSetupAsync(
|
||||
string repositoryRoot,
|
||||
string runtimeRoot,
|
||||
string resourceDirectory,
|
||||
bool replaceExistingLocalConfiguration)
|
||||
{
|
||||
var setupScript = Path.Combine(
|
||||
repositoryRoot,
|
||||
SetupScriptRelativePath);
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ResolveWindowsPowerShellPath(),
|
||||
WorkingDirectory = repositoryRoot,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
foreach (var argument in new[]
|
||||
{
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
setupScript,
|
||||
"-LegacyRuntimeSourceRoot",
|
||||
runtimeRoot,
|
||||
"-NoFolderPicker",
|
||||
"-DatabaseIniPath",
|
||||
Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
|
||||
"-ConfigureDevelopmentLive",
|
||||
"-PlayoutHost",
|
||||
DefaultPlayoutHost,
|
||||
"-PlayoutPort",
|
||||
DefaultPlayoutPort.ToString(
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
"-PinRegisteredK3D"
|
||||
})
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
if (replaceExistingLocalConfiguration)
|
||||
{
|
||||
startInfo.ArgumentList.Add("-ReplaceRuntimeBinding");
|
||||
startInfo.ArgumentList.Add("-ReplaceDatabaseProfile");
|
||||
startInfo.ArgumentList.Add("-ReplaceLiveConfig");
|
||||
}
|
||||
|
||||
using var process = Process.Start(startInfo) ??
|
||||
throw new InvalidOperationException("Windows PowerShell did not start.");
|
||||
var standardOutput = ReadBoundedTailAsync(process.StandardOutput);
|
||||
var standardError = ReadBoundedTailAsync(process.StandardError);
|
||||
await process.WaitForExitAsync();
|
||||
await Task.WhenAll(standardOutput, standardError);
|
||||
|
||||
var diagnosticText =
|
||||
string.IsNullOrWhiteSpace(standardError.Result)
|
||||
? standardOutput.Result
|
||||
: standardError.Result;
|
||||
return new FirstRunSetupResult(
|
||||
process.ExitCode == 0,
|
||||
LastDiagnosticLine(diagnosticText),
|
||||
RequiresLocalConfigurationReplacement(diagnosticText));
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmLocalConfigurationReplacementAsync()
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
XamlRoot = Content.XamlRoot,
|
||||
Title = "기존 로컬 설정 업데이트",
|
||||
Content =
|
||||
"이 PC에 이 앱의 이전 runtime, DB 또는 Development Live 설정이 있습니다. " +
|
||||
"선택한 Cuts/Res와 기본 Tornado2(127.0.0.1:30001) 설정으로 갱신할까요? " +
|
||||
"K3D DLL 기준값은 자동으로 바꾸지 않습니다.",
|
||||
PrimaryButtonText = "업데이트",
|
||||
CloseButtonText = "취소",
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
return await dialog.ShowAsync() == ContentDialogResult.Primary;
|
||||
}
|
||||
|
||||
private static bool RequiresLocalConfigurationReplacement(
|
||||
string? diagnostic) =>
|
||||
!string.IsNullOrWhiteSpace(diagnostic) &&
|
||||
(diagnostic.Contains(
|
||||
"-ReplaceRuntimeBinding",
|
||||
StringComparison.Ordinal) ||
|
||||
diagnostic.Contains(
|
||||
"-ReplaceDatabaseProfile",
|
||||
StringComparison.Ordinal) ||
|
||||
diagnostic.Contains(
|
||||
"-ReplaceLiveConfig",
|
||||
StringComparison.Ordinal));
|
||||
|
||||
private static string ResolveWindowsPowerShellPath()
|
||||
{
|
||||
var systemDirectory = Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.System);
|
||||
if (string.IsNullOrWhiteSpace(systemDirectory))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Windows system directory is unavailable.");
|
||||
}
|
||||
|
||||
var path = Path.GetFullPath(Path.Combine(
|
||||
systemDirectory,
|
||||
"WindowsPowerShell",
|
||||
"v1.0",
|
||||
"powershell.exe"));
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"Windows PowerShell 5.1 was not found.",
|
||||
path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadBoundedTailAsync(StreamReader reader)
|
||||
{
|
||||
var tail = new StringBuilder(MaximumDiagnosticCharacters);
|
||||
var buffer = new char[4096];
|
||||
int count;
|
||||
while ((count = await reader.ReadAsync(buffer)) > 0)
|
||||
{
|
||||
tail.Append(buffer, 0, count);
|
||||
if (tail.Length > MaximumDiagnosticCharacters)
|
||||
{
|
||||
tail.Remove(
|
||||
0,
|
||||
tail.Length - MaximumDiagnosticCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
return tail.ToString();
|
||||
}
|
||||
|
||||
private static string? LastDiagnosticLine(string diagnostic)
|
||||
{
|
||||
var line = diagnostic
|
||||
.Split(
|
||||
['\r', '\n'],
|
||||
StringSplitOptions.RemoveEmptyEntries |
|
||||
StringSplitOptions.TrimEntries)
|
||||
.LastOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return line.Length <= 400 ? line : string.Concat(line[..397], "...");
|
||||
}
|
||||
|
||||
private void SetSetupRunning(bool running)
|
||||
{
|
||||
_setupRunning = running;
|
||||
ChooseCutsButton.IsEnabled = !running;
|
||||
ChooseResourceButton.IsEnabled = !running;
|
||||
InstallButton.IsEnabled = !running &&
|
||||
_cutsDirectory is not null &&
|
||||
_resourceDirectory is not null;
|
||||
SetupProgress.Visibility = running
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ShowWarning(string message)
|
||||
{
|
||||
SetupStatusBar.Title = "확인 필요";
|
||||
SetupStatusBar.Message = message;
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Warning;
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
SetupStatusBar.Title = "설정 실패";
|
||||
SetupStatusBar.Message = message;
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Error;
|
||||
}
|
||||
|
||||
private AppWindow ResolveAppWindow()
|
||||
{
|
||||
var windowHandle = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(windowHandle);
|
||||
return AppWindow.GetFromWindowId(windowId);
|
||||
}
|
||||
|
||||
private sealed record FirstRunSetupResult(
|
||||
bool Succeeded,
|
||||
string? Diagnostic,
|
||||
bool LocalConfigurationReplacementRequired);
|
||||
}
|
||||
@@ -24,18 +24,24 @@
|
||||
<ApplicationDisplayVersion>0.1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<Version>0.1.0</Version>
|
||||
<!--
|
||||
Source-only F5 runs from an MSIX layout, not necessarily from the clone.
|
||||
Embed this local, non-secret build path so the first-run window can invoke
|
||||
the reviewed setup scripts in the exact clone that Visual Studio built.
|
||||
-->
|
||||
<SourceRepositoryRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))</SourceRepositoryRoot>
|
||||
<!--
|
||||
Keep the legacy runtime assets outside this repository while reproducing the
|
||||
original executable-folder layout. A fresh source checkout builds in safe
|
||||
source-only DryRun mode when the sibling runtime tree is absent. Supplying an
|
||||
explicit root or creating a Release MSIX makes the assets required unless the
|
||||
caller explicitly selects Auto mode:
|
||||
original executable-folder layout. A fresh source checkout always builds the
|
||||
safe source-only first-run app until its generated local binding exists.
|
||||
Supplying an explicit root or creating a Release MSIX makes the assets
|
||||
required unless the caller explicitly selects Auto mode:
|
||||
/p:LegacyRuntimeSourceRoot="D:\path\to\MBN_STOCK_N\bin\Debug"
|
||||
/p:LegacyRuntimeAssetsMode=Auto
|
||||
-->
|
||||
<LegacyRuntimeSourceRootWasSpecified
|
||||
Condition="'$(LegacyRuntimeSourceRoot)' != ''">true</LegacyRuntimeSourceRootWasSpecified>
|
||||
<LegacyRuntimeSourceRoot Condition="'$(LegacyRuntimeSourceRoot)' == ''">$(MSBuildProjectDirectory)\..\..\..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug</LegacyRuntimeSourceRoot>
|
||||
<LegacyRuntimeSourceRoot Condition="'$(LegacyRuntimeSourceRoot)' == ''">$(MSBuildProjectDirectory)\.first-run-runtime-not-configured-0F72B3C8</LegacyRuntimeSourceRoot>
|
||||
<LegacyRuntimeSourceRoot>$([System.IO.Path]::GetFullPath('$(LegacyRuntimeSourceRoot)'))</LegacyRuntimeSourceRoot>
|
||||
<LegacyRuntimeAssetsMode
|
||||
Condition="'$(LegacyRuntimeAssetsMode)' == '' and
|
||||
@@ -69,6 +75,13 @@
|
||||
<LegacyResSourceRoot>$(LegacyRuntimeSourceRoot)\Res</LegacyResSourceRoot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyMetadata Include="SourceRepositoryRoot"
|
||||
Value="$(SourceRepositoryRoot)"
|
||||
Condition="'$(LegacyRuntimeAssetsEnabled)' != 'true' and
|
||||
'$(Configuration)' == 'Debug'" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Closed runtime contract: every active alias and non-external scene asset must exist. -->
|
||||
<LegacyRequiredScene Include="5001;N5001;5006;5011;5016;50160;5023;5024;5025;5026;5029;8018;8032;5032;5037;5074;5076;5077;5078;5079;5080;5081;5082;5083;5084;5085;5086;50860;5087;5088;6001;6067;8001;8002;8003;8035;8061;8040;8046;8051;8056;8067;5068;5070;5072" />
|
||||
@@ -193,7 +206,7 @@
|
||||
<Message Condition="'$(LegacyRuntimeAssetsMode)' == 'Auto' and
|
||||
'$(LegacyRuntimeAssetsEnabled)' != 'true'"
|
||||
Importance="high"
|
||||
Text="Legacy runtime assets were not found. Building the safe source-only DryRun app. Run scripts\Initialize-ExistingDevelopmentPc.ps1 before Development Live." />
|
||||
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="Verified full legacy runtime assets are enabled. Database credentials remain in the current user's LocalAppData overlay." />
|
||||
|
||||
@@ -81,10 +81,16 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
|
||||
var bootstrap = app.IndexOf(
|
||||
"DevelopmentLiveLaunchBootstrap.TryApply(",
|
||||
StringComparison.Ordinal);
|
||||
var constructWindow = app.IndexOf("_window = new MainWindow();", StringComparison.Ordinal);
|
||||
var constructSetupWindow = app.IndexOf(
|
||||
"new FirstRunSetupWindow()",
|
||||
StringComparison.Ordinal);
|
||||
var constructMainWindow = app.IndexOf(
|
||||
"new MainWindow();",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.True(bootstrap >= 0);
|
||||
Assert.True(constructWindow > bootstrap);
|
||||
Assert.True(constructSetupWindow > bootstrap);
|
||||
Assert.True(constructMainWindow > constructSetupWindow);
|
||||
Assert.Contains("#if DEBUG", app, StringComparison.Ordinal);
|
||||
Assert.Contains("const bool isDebugBuild = true;", app, StringComparison.Ordinal);
|
||||
Assert.Contains("const bool isDebugBuild = false;", app, StringComparison.Ordinal);
|
||||
@@ -106,6 +112,10 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
|
||||
"IsDevelopmentLiveLaunch = developmentLive.IsApplied;",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_window = showFirstRunSetup",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string AppRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp");
|
||||
|
||||
[Fact]
|
||||
public void SourceOnlyLaunchShowsOnlyTheNativeFirstRunWindow()
|
||||
{
|
||||
var app = File.ReadAllText(Path.Combine(AppRoot, "App.xaml.cs"))
|
||||
.ReplaceLineEndings("\n");
|
||||
|
||||
Assert.Contains(
|
||||
"var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild;\n" +
|
||||
" _window = showFirstRunSetup\n" +
|
||||
" ? new FirstRunSetupWindow()\n" +
|
||||
" : new MainWindow();",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"var launchArguments = isSourceOnlyBuild\n" +
|
||||
" ? null",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"new FirstRunSetupWindow(App.IsDevelopmentLiveLaunch",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstRunUiRequestsOnlyCutsAndTheWholeResFolder()
|
||||
{
|
||||
var xaml = XDocument.Load(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml"));
|
||||
XNamespace presentation =
|
||||
"http://schemas.microsoft.com/winfx/2006/xaml/presentation";
|
||||
var buttons = xaml.Descendants(presentation + "Button")
|
||||
.Select(element => element.Attribute("Content")?.Value)
|
||||
.Where(value => value is not null)
|
||||
.ToArray();
|
||||
var source = File.ReadAllText(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml"));
|
||||
|
||||
Assert.Contains("Cuts 폴더 선택", buttons);
|
||||
Assert.Contains("Res 폴더 선택", buttons);
|
||||
Assert.DoesNotContain("INI 파일 선택", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("비밀번호", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("포트 입력", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("해시 입력", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BothFoldersAndAllActiveIniContractsAreValidatedBeforeSetup()
|
||||
{
|
||||
var setup = File.ReadAllText(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml.cs"));
|
||||
var settings = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyApplication",
|
||||
"LegacyOperatorSettings.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
"LegacyOperatorSettingsFolderKind.Scene",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"LegacyOperatorSettingsFolderKind.Resource",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"validation.Folder.DatabaseIniDetected",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new LegacyIniDatabaseOptionsLoader",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
var clearCuts = setup.IndexOf(
|
||||
"_cutsDirectory = null;",
|
||||
StringComparison.Ordinal);
|
||||
var validateCuts = setup.IndexOf(
|
||||
"LegacyOperatorSettingsFolderKind.Scene",
|
||||
StringComparison.Ordinal);
|
||||
var clearResource = setup.IndexOf(
|
||||
"_resourceDirectory = null;",
|
||||
StringComparison.Ordinal);
|
||||
var validateResource = setup.IndexOf(
|
||||
"LegacyOperatorSettingsFolderKind.Resource",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(clearCuts >= 0);
|
||||
Assert.True(validateCuts > clearCuts);
|
||||
Assert.True(clearResource > validateCuts);
|
||||
Assert.True(validateResource > clearResource);
|
||||
Assert.Contains(
|
||||
"Path.GetFileName(cuts),\n" +
|
||||
" \"Cuts\"",
|
||||
setup.ReplaceLineEndings("\n"),
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Path.GetFileName(resource),\n" +
|
||||
" \"Res\"",
|
||||
setup.ReplaceLineEndings("\n"),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
foreach (var ini in new[]
|
||||
{
|
||||
"종목.ini",
|
||||
"업종_코스피.ini",
|
||||
"업종_코스닥.ini",
|
||||
"해외.ini",
|
||||
"환율.ini",
|
||||
"지수.ini",
|
||||
"종목비교.ini"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{ini}\"", settings, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetupUsesArgumentListAndPinsTheVerifiedLocalK3dAtTheDefaultEndpoint()
|
||||
{
|
||||
var setup = File.ReadAllText(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
"startInfo.ArgumentList.Add(argument);",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("\"-NoFolderPicker\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-DatabaseIniPath\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ConfigureDevelopmentLive\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"private const string DefaultPlayoutHost = \"127.0.0.1\";",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"private const int DefaultPlayoutPort = 30001;",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("\"-PinRegisteredK3D\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ReplaceRuntimeBinding\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ReplaceDatabaseProfile\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ReplaceLiveConfig\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"result.LocalConfigurationReplacementRequired",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"await ConfirmLocalConfigurationReplacementAsync()",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"-ReplaceK3DPin\"", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"-SkipBuild\"", setup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceOnlyBuildEmbedsOnlyItsOwnClonePathForTheReviewedSetupScript()
|
||||
{
|
||||
var project = XDocument.Load(Path.Combine(
|
||||
AppRoot,
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp.csproj"));
|
||||
var metadata = Assert.Single(project.Descendants("AssemblyMetadata"));
|
||||
|
||||
Assert.Equal("SourceRepositoryRoot", metadata.Attribute("Include")?.Value);
|
||||
Assert.Equal("$(SourceRepositoryRoot)", metadata.Attribute("Value")?.Value);
|
||||
Assert.Contains(
|
||||
"'$(LegacyRuntimeAssetsEnabled)' != 'true'",
|
||||
metadata.Attribute("Condition")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"'$(Configuration)' == 'Debug'",
|
||||
metadata.Attribute("Condition")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutomaticK3dPinningIsPersistentAndCannotApproveAChangedDll()
|
||||
{
|
||||
var initializer = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"scripts",
|
||||
"Initialize-ExistingDevelopmentPc.ps1"));
|
||||
|
||||
Assert.Contains("[switch] $PinRegisteredK3D", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[switch] $ReplaceK3DPin", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Join-Path $ConfigurationRoot 'k3d-pins.local.json'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$approvedNativeSha256 = $actualNativeHash",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$approvedInteropSha256 = $actualInteropHash",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-PinRegisteredK3D cannot be combined with independently",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Automatic pinning cannot approve a changed DLL.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-ReplaceK3DPin cannot be combined with -PinRegisteredK3D.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"A replacement requires independently supplied native and interop hashes.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Write-ProtectedUtf8TextAtomically",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"This is a durable trust anchor, not transactional runtime state.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"K3D persistent pin rollback failed:",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
var pinPreflight = initializer.LastIndexOf(
|
||||
"$k3dPinPreflight = Get-K3DPinPreflight",
|
||||
StringComparison.Ordinal);
|
||||
var pinWrite = initializer.LastIndexOf(
|
||||
"Write-ProtectedUtf8TextAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var livePreflight = initializer.LastIndexOf(
|
||||
"$livePreflight = Get-DevelopmentLivePreflight",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(pinPreflight >= 0);
|
||||
Assert.True(pinWrite > pinPreflight);
|
||||
Assert.True(livePreflight > pinWrite);
|
||||
Assert.Contains(
|
||||
"nativeSha256 = $ApprovedNativeHash.ToUpperInvariant()",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"interopSha256 = $ApprovedInteropHash.ToUpperInvariant()",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Get-VerifiedK3DRegistration -Reports $registrationReports",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Activator.CreateInstance", initializer,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
@@ -342,11 +342,11 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
sourceOnlyMessage.Attribute("Condition")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"source-only DryRun",
|
||||
"safe first-run setup app",
|
||||
sourceOnlyMessage.Attribute("Text")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Initialize-ExistingDevelopmentPc.ps1",
|
||||
"select the existing Cuts and Res folders",
|
||||
sourceOnlyMessage.Attribute("Text")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
@@ -434,7 +434,7 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
element => element.Attribute("Condition") is not null);
|
||||
|
||||
Assert.Equal("'$(LegacyRuntimeSourceRoot)' == ''", sourceRoot.Attribute("Condition")!.Value);
|
||||
Assert.Contains("MBN_STOCK_N\\MBN_STOCK_N\\bin\\Debug", sourceRoot.Value,
|
||||
Assert.Contains(".first-run-runtime-not-configured-0F72B3C8", sourceRoot.Value,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var target = Assert.Single(
|
||||
@@ -787,7 +787,19 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("& dotnet build $ProjectPath -c Debug '-p:Platform=x64'",
|
||||
Assert.Contains("& $DotNetPath build $ProjectPath",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"'-p:LegacyRuntimeAssetsMode=Required'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$runtimeRootProperty = \"-p:LegacyRuntimeSourceRoot=$ExpectedRuntimeRoot\"",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Get-Command -Name 'dotnet.exe' -CommandType Application",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
@@ -808,23 +820,31 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
var runtimeWrite = initializer.LastIndexOf(
|
||||
"Set-RuntimeBindingAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var pinWrite = initializer.LastIndexOf(
|
||||
"Write-ProtectedUtf8TextAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var databaseWrite = initializer.LastIndexOf(
|
||||
"Copy-DatabaseProfileAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var build = initializer.LastIndexOf(
|
||||
"Invoke-DebugBuildAndVerify",
|
||||
StringComparison.Ordinal);
|
||||
var cleanup = initializer.LastIndexOf(
|
||||
"-Path $rollbackRoot",
|
||||
var finalK3dVerification = initializer.LastIndexOf(
|
||||
"$finalNativeHash = (",
|
||||
StringComparison.Ordinal);
|
||||
var liveWrite = initializer.LastIndexOf(
|
||||
"& $liveInitializerPath @liveArguments",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(runtimeWrite >= 0);
|
||||
Assert.True(databaseWrite > runtimeWrite);
|
||||
Assert.True(build > databaseWrite);
|
||||
Assert.True(cleanup > build);
|
||||
Assert.True(liveWrite > cleanup);
|
||||
var cleanup = initializer.LastIndexOf(
|
||||
"-Path $rollbackRoot",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(pinWrite >= 0);
|
||||
Assert.True(build > pinWrite);
|
||||
Assert.True(databaseWrite > build);
|
||||
Assert.True(finalK3dVerification > databaseWrite);
|
||||
Assert.True(liveWrite > finalK3dVerification);
|
||||
Assert.True(runtimeWrite > liveWrite);
|
||||
Assert.True(cleanup > runtimeWrite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -856,6 +876,26 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
"$Description rollback did not restore the exact bytes and ACL.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"'Local\\MBN_STOCK_WEBVIEW.ExistingDevelopmentPcInitialization.v1'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Another existing-development-PC initialization is already running.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Development Live base configuration rollback failed:",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"The durable pin remains at",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"anchor could not be restored:",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Set-UserOnlyDirectoryAcl -Path $rollbackRoot",
|
||||
initializer,
|
||||
@@ -968,8 +1008,11 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
var buildVerification = initializer.LastIndexOf(
|
||||
"Invoke-DebugBuildAndVerify",
|
||||
StringComparison.Ordinal);
|
||||
var setupCleanup = initializer.LastIndexOf(
|
||||
"-Path $rollbackRoot",
|
||||
var pinIssue = initializer.LastIndexOf(
|
||||
"Write-ProtectedUtf8TextAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var finalK3dVerification = initializer.LastIndexOf(
|
||||
"$finalNativeHash = (",
|
||||
StringComparison.Ordinal);
|
||||
var liveIssue = initializer.LastIndexOf(
|
||||
"$liveResults = @(& $liveInitializerPath @liveArguments)",
|
||||
@@ -977,14 +1020,24 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
var liveJsonVerification = initializer.LastIndexOf(
|
||||
"-ExpectedJson ([string] $livePreflight.AuthorizationJson)",
|
||||
StringComparison.Ordinal);
|
||||
var bindingCommit = initializer.LastIndexOf(
|
||||
"Set-RuntimeBindingAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var setupCleanup = initializer.LastIndexOf(
|
||||
"-Path $rollbackRoot",
|
||||
StringComparison.Ordinal);
|
||||
var failClosedLiveRemoval = initializer.LastIndexOf(
|
||||
"Remove-DevelopmentLiveAuthorizationFailClosed",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(firstPersistentLiveInvalidation >= 0);
|
||||
Assert.True(pinIssue >= 0);
|
||||
Assert.True(pinIssue < firstPersistentLiveInvalidation);
|
||||
Assert.True(firstPersistentLiveInvalidation < setupDirectoryCreation);
|
||||
Assert.True(buildVerification < setupCleanup);
|
||||
Assert.True(setupCleanup < liveIssue);
|
||||
Assert.True(setupDirectoryCreation < buildVerification);
|
||||
Assert.True(buildVerification < finalK3dVerification);
|
||||
Assert.True(finalK3dVerification < liveIssue);
|
||||
Assert.True(liveIssue < liveJsonVerification);
|
||||
Assert.True(liveJsonVerification < bindingCommit);
|
||||
Assert.True(bindingCommit < setupCleanup);
|
||||
Assert.True(liveJsonVerification < failClosedLiveRemoval);
|
||||
Assert.Contains(
|
||||
"Development Live initializer returned unexpected success-stream output.",
|
||||
|
||||
Reference in New Issue
Block a user