From 11d384993397383d102599c026f050d83c7b755a Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 28 Jul 2026 01:03:55 +0900 Subject: [PATCH] fix: persist first-run folder paths only --- Directory.Build.props | 6 +- README.md | 55 +--- docs/DEVELOPMENT_LIVE_HANDOFF.md | 80 ++--- docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md | 28 +- docs/LEGACY_RUNTIME_ASSETS.md | 33 +- .../LegacyOperatorSettings.cs | 225 +------------ .../FirstRunSetupWindow.xaml | 6 +- .../FirstRunSetupWindow.xaml.cs | 311 ++++++------------ .../MBN_STOCK_WEBVIEW.LegacyParityApp.csproj | 4 +- .../MainWindow.Settings.cs | 25 -- .../LegacyOperatorSettingsTests.cs | 43 +-- .../LegacyFirstRunSetupNativeContractTests.cs | 86 +++-- .../LegacyOperatorSettingsWebContractTests.cs | 4 +- 13 files changed, 240 insertions(+), 666 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 97e397d..940e9c3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,9 +11,9 @@ diff --git a/README.md b/README.md index 49d3a3e..e2a8bb6 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,9 @@ Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑 4. 실행 프로필이 `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`인지 확인합니다. 5. `F5`로 빌드·배포·실행합니다. -6. 첫 실행 설정 창에서 기존 코더의 `Cuts` 폴더와 여러 INI가 함께 있는 `Res` 폴더, 두 - 폴더만 각각 선택하고 `설정 시작`을 누릅니다. -7. 이 앱의 이전 로컬 설정이 다른 경우에만 업데이트 확인 창이 한 번 표시됩니다. 선택한 - 두 폴더와 기본 Tornado2 설정으로 갱신하려면 `업데이트`를 누릅니다. K3D 기준값은 이 - 확인으로 바뀌지 않습니다. -8. `설정 완료`가 표시되면 창을 닫고 `F5`를 한 번 더 누릅니다. +6. 첫 실행 설정 창에서 기존 코더의 `Cuts` 폴더와 `Res` 폴더, 두 폴더만 각각 선택하고 + `설정 시작`을 누릅니다. +7. `설정 완료`가 표시되면 창을 닫고 `F5`를 한 번 더 누릅니다. 새로 받은 clone에 아직 이 PC의 검증된 runtime 연결이 없으면, 주변에 과거 `MBN_STOCK_N\...\bin\Debug`가 있더라도 첫 F5는 안전한 @@ -86,52 +83,36 @@ Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중 필요가 없습니다. 첫 실행 창에는 다음 두 폴더만 입력합니다. - `Cuts`: 실제 실행 자산의 `Cuts` 폴더 -- `설정/INI (Res)`: `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, - `업종_코스닥.ini`, `해외.ini`, `환율.ini`, `지수.ini`, `종목비교.ini`가 함께 있는 - 실제 `Res` 폴더 +- `설정/INI (Res)`: 기존 코더가 사용하는 실제 `Res` 폴더 -개별 INI를 따로 고르는 입력은 없습니다. 첫 실행 설정이 위 **활성 INI 8개**를 모두 -확인하며, `MmoneyCoder.ini`는 DB 설정으로, 나머지 7개는 컷/UI 구성 정보로 처리합니다. +첫 실행 설정은 두 폴더 안의 파일을 열거·파싱·복사하지 않습니다. 폴더 경로만 +`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`에 저장하고, +같은 공통 부모를 Git 제외 `Directory.Build.local.props`에 기록합니다. 두 폴더는 같은 공통 부모 아래에 나란히 있어야 하며 폴더 이름도 각각 `Cuts`, `Res`여야 합니다. 소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더가 아니라 기존 코더가 실제 실행에 사용하던 `bin\Debug\Cuts`, `bin\Debug\Res` 성격의 폴더를 선택합니다. -초기화기는 기존 자산을 검증한 뒤 사용자 전용 LocalAppData에 검증된 runtime 복사본을 만들고, -그 경로를 명시해 전체 Debug 빌드를 먼저 검증합니다. Git 제외 -`Directory.Build.local.props`는 DB/K3D/Development Live 설정까지 성공한 뒤 마지막 성공 -표식으로 기록되어, 중간에 Visual Studio가 종료되면 다음 F5도 첫 실행 설정 화면으로 -돌아옵니다. -DB INI도 사용자 전용 LocalAppData overlay로 복사해 ACL을 보호하고, K3D x64 등록을 읽기 -전용으로 점검합니다. 예상 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이 성공 표식으로 기록됩니다. +`설정 시작`은 PowerShell, `dotnet build`, DB 복사, K3D 검사 또는 Tornado2/PGM 작업을 +실행하지 않으므로 즉시 완료되어야 합니다. 전체 앱 빌드는 창을 닫고 누르는 다음 F5에서 +진행됩니다. 이때도 `Cuts`는 복사하지 않고 선택한 원래 경로를 직접 사용합니다. -테스트 개발 서버라 자격증명 노출 위험을 수용하는 경우에도 저장소 계약은 바뀌지 않습니다. -`MmoneyCoder.ini`와 DB 자격증명은 사용자 전용 LocalAppData에만 두고 Git, 빌드 출력, -runtime bundle과 MSIX에는 포함하지 않습니다. - -Debug x64 빌드 출력은 선택한 manifest와 경로·길이·SHA-256·파일 집합이 정확히 일치하고 -SourceOnly가 아닌지까지 자동 확인합니다. 중간 단계가 실패하면 기존 runtime 연결과 DB -overlay를 원래 내용·ACL로 되돌리고 Live 승인 파일을 남기지 않습니다. 이 과정은 Tornado에 -`CONNECT`하거나 장면을 송출하지 않습니다. +Development Live용 DB overlay, K3D pin과 한 회차 승인 파일이 아직 없는 PC는 별도의 +`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 안전한 `DryRun`으로 +시작합니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다. 초기화가 성공하면 Visual Studio에서 `Debug|x64`, `Legacy Parity App (VS F5)`, `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작합니다. -앱이나 빌드 출력이 `SourceOnly` 또는 `DryRun`이라고 표시되면 송출을 시도하지 말고 초기화를 -바로잡습니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라 +앱이나 빌드 출력이 `SourceOnly` 또는 `DryRun`이라고 표시되면 송출을 시도하지 말고 별도 +Development Live 준비 상태를 확인합니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라 `5001` 하나로 진행합니다. Development Live는 이 Debug 프로세스에만 적용되며 Release와 기본 프로필은 계속 `DryRun`입니다. 이 프로필은 보호된 LocalAppData 설정만 읽고 상속된 -`MBN_STOCK_PLAYOUT_*`·DB 환경 변수와 기존 운영자 scene/resource/background 선택을 무시합니다. -endpoint는 숫자형 loopback, scene root는 실행 파일 옆 검증된 `Cuts`로 다시 고정하며 strict +`MBN_STOCK_PLAYOUT_*`·DB 환경 변수를 무시합니다. scene root는 첫 실행에서 저장한 원래 +`Cuts` 경로로 고정하며 strict 검증이 실패하면 Live를 승인하지 않고 안전한 DryRun으로 시작합니다. 같은 clone에서는 로컬 연결 파일이 Git에 덮어써지지 않으므로 이후에는 `git pull` 후 F5로 확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB 폴더 연결이 바뀌면 첫 실행 -설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 첫 실행 설정을 반복해도 자동 +설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 경로 저장을 반복해도 자동 재승인되지 않습니다. 벤더 변경을 독립적으로 검증한 뒤 두 SHA-256을 명시해 수동 교체해야 합니다. pull 뒤 Required 자산 검증이 실패하는 경우에도 F5를 계속 시도하지 말고 설정 오류를 바로잡습니다. diff --git a/docs/DEVELOPMENT_LIVE_HANDOFF.md b/docs/DEVELOPMENT_LIVE_HANDOFF.md index 7058d36..0a7a51c 100644 --- a/docs/DEVELOPMENT_LIVE_HANDOFF.md +++ b/docs/DEVELOPMENT_LIVE_HANDOFF.md @@ -60,54 +60,23 @@ PowerShell 명령은 필요 없다. 3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB 미연결과 DryRun을 강제하고 Tornado2/PGM에 연결하지 않는다. 4. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다. -5. `Res 폴더 선택`에서 다음 파일들이 함께 있는 실제 설정 폴더를 선택한다. - `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`, `해외.ini`, - `환율.ini`, `지수.ini`, `종목비교.ini`. +5. `Res 폴더 선택`에서 기존 코더가 사용하는 실제 `Res` 폴더를 선택한다. 6. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다. 7. `설정 완료`가 표시되면 창을 닫고 F5를 한 번 더 누른다. -소스 트리의 오래된 `RES`, 백업 폴더, `MmoneyCoder - 복사본.ini`가 있는 다른 위치를 -선택하지 않는다. 화면 입력은 `Cuts`와 `Res` 두 폴더뿐이지만, 첫 실행 앱은 -`MmoneyCoder.ini`를 포함한 **활성 INI 8개**를 모두 확인한다. 7개 UI INI는 각각 파싱하고 -`MmoneyCoder.ini`는 DB 계약을 검증한다. 전체 Res를 무차별 복사하지 않고 폐쇄형 34개 -allowlist만 runtime으로 구성하며, 자격증명 INI는 별도 사용자 전용 경로로만 복사한다. -개발 테스트 서버의 계정이라 노출을 허용하는 경우에도 자격증명을 Git이나 빌드 산출물에 넣지 -않는 계약은 그대로 유지한다. +소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더를 선택하지 않는다. `설정 시작`은 +두 폴더의 고정 로컬 경로만 다음 위치에 저장한다. -초기화기는 다음 작업을 한 번에 수행해야 한다. +- `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json` +- 저장소의 Git 제외 `Directory.Build.local.props` -- 선택한 `Cuts`와 `Res`의 고정 로컬 경로를 - `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`에 저장한다. - `Cuts`는 원래 폴더를 그대로 사용하며 전체 파일 열거, ZIP 생성, 복사, 크기·해시 검사를 - 하지 않는다. 실제 장면이나 영상이 사용될 때 그 파일만 개별 검증하므로 사용하지 않는 - 파일이 없거나 `Cuts\Video`가 일부 비어 있어도 초기화가 중단되지 않는다. -- 두 폴더의 공통 부모를 명시적 MSBuild 속성으로 전달해, 저장소 binding을 아직 바꾸지 않은 - 상태에서 Debug x64 빌드를 먼저 검증한다. 빌드 출력에는 `Cuts` 복사본을 만들지 않는다. -- 기존 DB INI를 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini`로 복사하고 현재 - Windows 사용자 전용 ACL을 적용한다. -- K3D x64 등록 상태를 읽기 전용으로 점검하고 검증된 설치본의 native/Interop SHA-256을 - 사용자 전용 `Config\k3d-pins.local.json`의 최초 기준으로 자동 고정한다. 이 파일이 이미 - 있으면 같은 값을 재사용하고, 등록 DLL이 달라졌을 때 자동 갱신하지 않는다. -- Debug x64 빌드 후 선택한 runtime root가 직접 적용되었는지, SourceOnly가 아닌지, - `Cuts` 복사본과 DB INI가 빌드 출력에 들어가지 않았는지 검증한다. `Res`는 UI에 필요한 - 폐쇄형 allowlist만 빌드 출력에 배치한다. -- `127.0.0.1:30001` 개발 기본 endpoint의 보호된 로컬 Development Live 설정을 모든 선행 - 검증 뒤 새로 발급한다. -- 저장소에서 제외되는 `Directory.Build.local.props`를 마지막 성공 표식으로 원자 기록해 - 선택한 두 폴더의 공통 부모를 `LegacyRuntimeAssetsMode=Required`로 사용하게 한다. +이 버튼은 폴더 안의 파일을 열거·파싱·복사하지 않으며 PowerShell, `dotnet build`, DB 복사, +K3D 검사, Live 승인 발급 또는 Tornado2/PGM 명령을 실행하지 않는다. 따라서 경로 저장은 +즉시 완료되어야 한다. `Cuts`와 `Res`의 실제 파일 검사는 다음 F5의 빌드 또는 해당 기능을 +사용하는 시점에 필요한 파일 단위로 수행된다. -초기화 과정 자체는 Tornado2/PGM에 `CONNECT`하거나 PREPARE, TAKE IN, NEXT, TAKE OUT 등의 -명령을 보내지 않는다. 초기화 도중 오류가 나면 F5로 송출을 시도하지 말고 원인부터 바로잡는다. -runtime 연결이나 DB overlay를 갱신한 뒤 선행 단계가 실패하면 기존 파일의 정확한 내용과 -ACL을 복구하고 Live 승인 파일은 제거된 상태로 둔다. 반면 K3D pin은 DLL 변경 뒤 재시도로 -자동 승인되는 일을 막는 지속 신뢰 기준이므로, 최초 생성 또는 독립 검증에 따른 명시적 교체가 -완료된 뒤 다른 설정 단계가 실패해도 그대로 보존한다. -`Directory.Build.local.props`는 모든 기능 설정이 끝난 뒤에만 기록하므로, 설정 창이나 -Visual Studio가 그 전에 비정상 종료되면 다음 F5도 SourceOnly 첫 실행 화면으로 돌아와 -같은 두 폴더로 다시 진행할 수 있다. - -기본 초기화가 이미 빌드를 확인한다. 필요하면 같은 조건이 유지되는지 별도 MSBuild 인자나 환경 -변수 없이 다음 명령으로 다시 확인할 수 있다. +전체 Debug x64 빌드는 설정 창을 닫고 누르는 다음 F5에서 진행된다. 필요하면 같은 조건을 +별도 MSBuild 인자나 환경 변수 없이 다음 명령으로 확인할 수 있다. ```powershell dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj ` @@ -115,19 +84,23 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar -p:Platform=x64 ``` -출력에 전체 runtime 자산이 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드, -`bin\SourceOnly` 또는 `DryRun`만 표시되면 Development Live를 진행하지 않는다. +출력에 외부 runtime 경로가 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드가 계속 +표시되면 Visual Studio에서 다시 빌드한다. `DryRun`은 경로 저장 실패가 아니라 별도의 +Development Live 승인이 아직 없다는 뜻일 수 있다. 같은 clone에서는 이후 `git pull` 뒤 Visual Studio 2026에서 `Debug|x64`, 시작 대상 `Legacy Parity App (VS F5)`, 실행 프로필 `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작할 수 -있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 설정을 다시 진행한다. 자산·DB·K3D·PGM -설정이 바뀌었거나 pull 뒤 Required 자산 검증이 실패하면 F5를 반복하지 말고 변경 원인을 -검토한다. 특히 K3D DLL 변경은 첫 실행 화면을 반복해도 자동 승인되지 않는다. 최초 실제 동작 +있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 경로 저장을 다시 진행한다. DB overlay, +K3D pin과 Development Live 승인이 없는 새 PC에서는 아래 수동 초기화 절차를 별도로 +완료하기 전까지 안전한 `DryRun`을 유지한다. K3D DLL 변경은 경로 저장을 반복해도 자동 +승인되지 않는다. 최초 실제 동작 확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다. -### 화면 초기화를 사용할 수 없을 때 +### Development Live까지 준비할 때 -첫 실행 창을 열 수 없는 장애 대응에만 저장소 루트에서 다음 수동 등가 명령을 사용한다. +첫 실행 경로 저장과 별개로 DB overlay, K3D pin과 Development Live 승인까지 새로 +준비해야 할 때 저장소 루트에서 다음 수동 명령을 사용한다. 이 명령은 전체 Debug 빌드 +검증까지 수행하므로 단순 폴더 저장 버튼보다 오래 걸리는 것이 정상이다. ```powershell powershell -NoProfile -ExecutionPolicy Bypass ` @@ -143,9 +116,8 @@ powershell -NoProfile -ExecutionPolicy Bypass ` 기존 로컬 runtime, DB 또는 Live 설정과 다르면 자동으로 덮어쓰지 않는다. 변경 대상을 검토한 뒤에만 `-ReplaceRuntimeBinding`, `-ReplaceDatabaseProfile`, `-ReplaceLiveConfig` 중 필요한 항목을 명시한다. 수동 모드는 기존처럼 독립 승인된 `-NativeSha256`과 `-InteropSha256`을 -사용할 수도 있으며, 이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 이 스크립트는 첫 -실행 화면이 내부에서 사용하는 초기화 도구의 수동 fallback이며 일반 clone 절차의 선행 -명령이 아니다. +사용할 수도 있으며, 이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 이 스크립트는 +첫 실행 화면에서 자동으로 호출되지 않는다. ## 3. 자산이 없는 PC 대안: 검증된 Git 밖 runtime bundle 설치 @@ -337,8 +309,8 @@ runtime이나 DB 위치도 의도적으로 달라졌을 때만 각각 `-ReplaceR ## 6. 보호된 로컬 송출 설정 -2단계 첫 실행 설정은 지속 K3D pin을 확인한 뒤 아래 송출 설정 두 파일도 마지막 기능 설정 -단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug 빌드가 모두 +별도 수동 Development Live 초기화는 지속 K3D pin을 확인한 뒤 아래 송출 설정 두 파일도 +마지막 기능 설정 단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug 빌드가 모두 성공하지 않으면 새 Live 승인 파일을 남기지 않는다. 자산 없는 PC의 3단계 bundle 흐름처럼 설정만 별도로 초기화해야 할 때는 저장소 루트에서 다음 스크립트를 실행한다. 이 수동 경로의 두 해시는 별도로 확인한 값이어야 한다. 지정 개발 PC의 endpoint는 diff --git a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md index 7b6ae62..4bbf3d9 100644 --- a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md +++ b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md @@ -24,21 +24,21 @@ 새 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`로 고정된다. 선택한 경로는 사용자 설정에 직접 저장하며 `Cuts` 전체를 -열거·복사·압축·해시 검사하지 않는다. 필요한 장면과 영상 파일은 실제 사용 시에만 개별 -확인하므로 일부 파일이 없어도 설정은 완료된다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 -누른다. 두 번째 F5부터 위 세 조건을 검증해 Development Live를 적용한다. 자세한 절차는 +화면에서 기존 코더의 `Cuts`와 `Res` 폴더, 두 개만 선택한다. 설정 버튼은 폴더 안의 파일을 +열거·파싱·복사하지 않고 두 경로와 로컬 MSBuild binding만 저장한다. PowerShell, +`dotnet build`, DB 복사, K3D 검사와 Live 승인 발급도 실행하지 않으므로 즉시 완료되어야 +한다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 전체 앱 빌드가 시작된다. + +기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서도 안전한 `DryRun`을 유지한다. +DB overlay, K3D pin과 Development Live 승인이 필요하면 아래 수동 초기화 절차를 별도로 +완료한다. 자세한 절차는 [개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를 따른다. `MmoneyCoder.ini`의 DB 자격증명은 테스트 개발 서버용이라도 Git에 넣지 않는다. 첫 실행 -설정은 사용자 전용 LocalAppData overlay로만 복사하며 빌드 출력, 선택 경로 설정과 MSIX에는 -포함하지 않는다. 첫 실행 화면이 열리지 않거나 기존 로컬 설정을 명시적으로 교체해야 할 때만 -`Initialize-ExistingDevelopmentPc.ps1`을 수동 fallback으로 사용한다. +경로 저장은 이 파일을 복사하거나 읽지 않는다. 빌드 출력, 선택 경로 설정과 MSIX에도 +포함하지 않는다. 보호된 LocalAppData DB overlay와 Live 설정이 필요할 때만 +`Initialize-ExistingDevelopmentPc.ps1`을 수동으로 사용한다. ## Visual Studio 시작 대상과 프로필 @@ -73,7 +73,7 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴 ### 지속 K3D pin -첫 실행 설정은 K3D의 HKLM x64 등록, CLSID·ProgID·TypeLib, AMD64와 vendor 배치를 먼저 +수동 Development Live 초기화는 K3D의 HKLM x64 등록, CLSID·ProgID·TypeLib, AMD64와 vendor 배치를 먼저 검증한다. 검사를 통과한 최초 native/Interop DLL의 SHA-256은 다음 사용자 전용 파일에 지속 저장한다. @@ -81,7 +81,7 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴 %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json ``` -이후 등록 DLL의 해시가 이 기준과 다르면 첫 실행 설정을 다시 눌러도 자동으로 pin을 바꾸지 +이후 등록 DLL의 해시가 이 기준과 다르면 폴더 경로를 다시 저장해도 자동으로 pin을 바꾸지 않고 Development Live를 차단한다. 정식 DLL 교체 시에는 벤더 배포본 등으로 독립 검증한 `-NativeSha256`과 `-InteropSha256`을 제공하고 `Initialize-ExistingDevelopmentPc.ps1 -ReplaceK3DPin -ReplaceLiveConfig` 수동 절차를 @@ -126,7 +126,7 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴 수동 생성·교체 시에는 현재 설치 파일의 해시를 그 자리에서 계산했다는 이유만으로 승인 값으로 사용해서는 안 된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 일반 -첫 실행에서는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤 같은 기준값으로 이 승인 파일을 +수동 Development Live 초기화에서는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤 같은 기준값으로 이 승인 파일을 발급한다. 파일 ACL은 현재 개발 사용자와 관리자만 수정할 수 있도록 유지한다. ## 기존 playout.local.json diff --git a/docs/LEGACY_RUNTIME_ASSETS.md b/docs/LEGACY_RUNTIME_ASSETS.md index 911fde4..d581168 100644 --- a/docs/LEGACY_RUNTIME_ASSETS.md +++ b/docs/LEGACY_RUNTIME_ASSETS.md @@ -26,9 +26,8 @@ Web\... 기본 `LegacyRuntimeAssetsMode`는 `Auto`다. -- 초기화기가 선택한 두 경로와 빌드를 검증한 뒤 마지막 성공 표식으로 만든 Git 제외 - `Directory.Build.local.props`가 있으면, 그 공통 부모 아래의 원래 `Cuts`와 `Res` 경로를 - 직접 사용한다. +- 첫 실행 창이 선택한 두 경로를 저장하고 만든 Git 제외 `Directory.Build.local.props`가 + 있으면, 그 공통 부모 아래의 원래 `Cuts`와 `Res` 경로를 직접 사용한다. - 새 Git clone의 Debug/F5는 주변의 과거 원본 경로를 자동 선택하지 않고 소스 전용 첫 실행 설정 앱을 만든다. - 원본 경로를 명시하거나 Release MSIX를 만들면 기본값이 `Required`로 바뀌어 누락 자산을 @@ -38,15 +37,13 @@ Web\... 표시한다. 이 프로세스는 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으로 사용한다. +첫 실행 화면은 선택한 두 폴더의 고정 로컬 경로와 `Cuts`/`Res` 이름·공통 부모만 확인한다. +폴더 내부 파일을 열거·파싱·복사하지 않고 PowerShell, 빌드, DB, K3D 또는 Live 설정도 +실행하지 않는다. DB overlay와 Development Live 준비가 필요한 경우에만 +`Initialize-ExistingDevelopmentPc.ps1`을 별도 수동 절차로 사용한다. ```text src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\SourceOnly\... @@ -90,7 +87,7 @@ Git에는 `Cuts`, DB 자격증명, 벤더 DLL, 라이선스, 인증서 또는 좌측 메뉴 하단의 `설정`에서는 네이티브 폴더 선택 창으로 디자인(`Cuts`) 폴더, 설정(`Res`) 폴더와 선택 사항인 운영 배경 폴더를 직접 지정할 수 있다. 선택한 고정 로컬 경로와 `Res` -카탈로그 형식을 확인한 뒤 다음 앱 전용 파일에 저장한다. `Cuts` 내용 전체는 검사하지 않는다. +폴더 자체만 확인한 뒤 다음 앱 전용 파일에 저장한다. 폴더 내용 전체는 검사하지 않는다. ```text %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json @@ -100,7 +97,7 @@ Git에는 `Cuts`, DB 자격증명, 벤더 DLL, 라이선스, 인증서 또는 적용된다. 같은 화면의 `왼쪽 메뉴 펼쳐서 시작`은 즉시 화면에 반영되고 다음 시작 상태도 위 파일에 저장된다. 절대 경로는 Web 콘텐츠에 전달하지 않는다. -이 화면은 자산 위치, 메뉴 시작 상태와 선택한 `Res` 안의 검증된 DB 프로필 위치만 관리한다. +이 화면은 자산 위치와 메뉴 시작 상태만 관리한다. DB 자격증명의 내용은 표시하거나 직접 편집하지 않는다. Release/default `DryRun`, KTAP 호스트· 포트·채널, Test/Live 게이트, scene allowlist와 벤더 해시는 보호 설정으로 유지되며 폴더를 선택해도 완화되거나 변경되지 않는다. @@ -119,13 +116,13 @@ CP949 레거시 형식으로 읽고, 파일의 section/row/order를 기존의 %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini ``` -이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 설정은 -원본 `MmoneyCoder.ini`를 위 사용자 전용 경로에만 복사하고 현재 Windows 사용자 전용 ACL로 -보호한다. DB 자격증명은 Git, Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지 -않는다. +이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 폴더 +저장은 원본 `MmoneyCoder.ini`를 읽거나 복사하지 않는다. 수동 Development Live 초기화에서 +필요할 때만 위 사용자 전용 경로에 복사하고 현재 Windows 사용자 전용 ACL로 보호한다. +DB 자격증명은 Git, Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지 않는다. -사용자 설정 메뉴에서 별도 `Res` 폴더를 선택했고 그 안에 `MmoneyCoder.ini`가 있으면 앱은 저장 -전에 형식을 네이티브에서 검증하고 다음 시작부터 그 DB 설정을 우선 사용한다. 파일 내용과 +사용자 설정 메뉴에서 별도 `Res` 폴더를 선택하면 앱은 경로만 저장한다. 일반 Debug/DryRun은 +다음 시작부터 그 폴더의 DB 설정을 사용할 수 있지만 형식 검사는 실제 로드 시 수행한다. 파일 내용과 자격증명은 Web 화면에 노출하지 않으며 화면에서 직접 편집할 수도 없다. 단, one-shot Gate A 검증 실행에서는 이 사용자 선택을 무시하고 검증 계획에 고정된 `database.local.json`만 사용한다. diff --git a/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs b/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs index 4a9eac0..4261c45 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs @@ -93,20 +93,6 @@ public static class LegacyOperatorFolderValidator private const string InvalidFolderMessage = "선택한 폴더를 안전하게 사용할 수 없습니다."; - private const string InvalidResourceMessage = - "선택한 설정 폴더의 UI 설정 파일을 검증할 수 없습니다."; - - private static readonly string[] ResourceFileNames = - [ - "종목.ini", - "업종_코스피.ini", - "업종_코스닥.ini", - "해외.ini", - "환율.ini", - "지수.ini", - "종목비교.ini" - ]; - private static readonly IReadOnlyList RequiredSceneFileNameValues = Array.AsReadOnly(LegacySceneRuntimeCoverage.ExpectedCutCodes .Select(code => string.Concat(code, ".t2s")) @@ -184,54 +170,11 @@ public static class LegacyOperatorFolderValidator switch (kind) { case LegacyOperatorSettingsFolderKind.Scene: - // Cuts is an operator-owned external runtime directory. Individual - // scenes and media are optional and are validated only when a - // command actually resolves that file. - break; - case LegacyOperatorSettingsFolderKind.Resource: - foreach (var fileName in ResourceFileNames) - { - if (!TryValidateRegularFile( - Path.Combine(canonicalPath, fileName), - required: true, - out failure)) - { - return Invalid(failure, InvalidResourceMessage); - } - } - - var stockMenu = LegacyRuntimeStockCutMenuLoader - .ResolveFromResourceDirectory(canonicalPath); - if (stockMenu.UsesBuiltInCatalog || - stockMenu.Failure != LegacyRuntimeStockCutMenuFailure.None) - { - return Invalid( - LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog, - InvalidResourceMessage); - } - - var uiCatalogs = LegacyRuntimeUiCatalogLoader - .ResolveFromResourceDirectory(canonicalPath); - if (uiCatalogs.Warnings.Count != 0) - { - return Invalid( - LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog, - InvalidResourceMessage); - } - - var databaseIniPath = Path.Combine(canonicalPath, "MmoneyCoder.ini"); - if (!TryInspectOptionalRegularFile( - databaseIniPath, - out databaseIniDetected, - out failure)) - { - return Invalid(failure, InvalidResourceMessage); - } - - break; - case LegacyOperatorSettingsFolderKind.Background: + // Folder selection stores only the canonical external path. + // Individual Cuts/Res files are resolved when the feature that + // needs them is used. break; default: @@ -385,168 +328,6 @@ public static class LegacyOperatorFolderValidator } } - private static bool TryValidateRegularFile( - string path, - bool required, - out LegacyOperatorFolderValidationFailure failure) - { - return TryValidateRegularFile(path, required, out _, out failure); - } - - private static bool TryValidateRegularFile( - string path, - bool required, - out bool detected, - out LegacyOperatorFolderValidationFailure failure) - { - detected = false; - try - { - var attributes = File.GetAttributes(path); - if ((attributes & (FileAttributes.Directory | - FileAttributes.ReparsePoint | - FileAttributes.Device)) != 0) - { - failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe; - return false; - } - - if (new FileInfo(path).Length <= 0) - { - failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe; - return false; - } - - detected = true; - failure = LegacyOperatorFolderValidationFailure.None; - return true; - } - catch (Exception exception) when ( - exception is FileNotFoundException or DirectoryNotFoundException) - { - failure = required - ? LegacyOperatorFolderValidationFailure.RequiredFileMissing - : LegacyOperatorFolderValidationFailure.None; - return !required; - } - catch (Exception exception) when ( - exception is ArgumentException or IOException or UnauthorizedAccessException or - NotSupportedException or System.Security.SecurityException) - { - failure = LegacyOperatorFolderValidationFailure.FileUnavailable; - return false; - } - } - - private static bool TryValidateContainedRegularFile( - string root, - string relativePath, - bool required, - out bool detected, - out LegacyOperatorFolderValidationFailure failure) - { - detected = false; - try - { - if (string.IsNullOrWhiteSpace(relativePath) || - Path.IsPathFullyQualified(relativePath) || - relativePath.Any(char.IsControl)) - { - failure = LegacyOperatorFolderValidationFailure.InvalidPath; - return false; - } - - var fullPath = Path.GetFullPath(Path.Combine(root, relativePath)); - var rootPrefix = string.Concat( - Path.TrimEndingDirectorySeparator(root), - Path.DirectorySeparatorChar); - if (!fullPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase)) - { - failure = LegacyOperatorFolderValidationFailure.InvalidPath; - return false; - } - - var relativeDirectory = Path.GetDirectoryName(relativePath); - if (!string.IsNullOrEmpty(relativeDirectory)) - { - var current = root; - foreach (var segment in relativeDirectory.Split( - [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], - StringSplitOptions.RemoveEmptyEntries)) - { - current = Path.Combine(current, segment); - try - { - var attributes = File.GetAttributes(current); - if ((attributes & FileAttributes.Directory) == 0 || - (attributes & (FileAttributes.ReparsePoint | - FileAttributes.Device)) != 0) - { - failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe; - return false; - } - } - catch (Exception exception) when ( - exception is FileNotFoundException or DirectoryNotFoundException) - { - break; - } - } - } - - return TryValidateRegularFile(fullPath, required, out detected, out failure); - } - catch (Exception exception) when ( - exception is ArgumentException or IOException or UnauthorizedAccessException or - NotSupportedException or System.Security.SecurityException) - { - failure = LegacyOperatorFolderValidationFailure.FileUnavailable; - return false; - } - } - - private static bool TryInspectOptionalRegularFile( - string path, - out bool detected, - out LegacyOperatorFolderValidationFailure failure) - { - detected = false; - try - { - var attributes = File.GetAttributes(path); - if ((attributes & (FileAttributes.Directory | - FileAttributes.ReparsePoint | - FileAttributes.Device)) != 0) - { - failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe; - return false; - } - - if (new FileInfo(path).Length <= 0) - { - failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe; - return false; - } - - detected = true; - failure = LegacyOperatorFolderValidationFailure.None; - return true; - } - catch (Exception exception) when ( - exception is FileNotFoundException or DirectoryNotFoundException) - { - failure = LegacyOperatorFolderValidationFailure.None; - return true; - } - catch (Exception exception) when ( - exception is ArgumentException or IOException or UnauthorizedAccessException or - NotSupportedException or System.Security.SecurityException) - { - failure = LegacyOperatorFolderValidationFailure.FileUnavailable; - return false; - } - } - private static LegacyOperatorFolderValidationResult Invalid( LegacyOperatorFolderValidationFailure failure, string warningMessage) => new( diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml index f56499f..a6d926e 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml @@ -23,7 +23,7 @@ @@ -66,7 +66,7 @@ Text="2. 설정/INI 폴더 (Res)" /> diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs index 85da2da..4d16f65 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs @@ -1,7 +1,6 @@ -using System.Diagnostics; using System.Reflection; using System.Text; -using MBN_STOCK_WEBVIEW.Infrastructure; +using System.Xml; using MBN_STOCK_WEBVIEW.LegacyApplication; using Microsoft.UI.Windowing; using Windows.Graphics; @@ -13,11 +12,8 @@ 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 const string LocalBuildPropsFileName = + "Directory.Build.local.props"; private string? _cutsDirectory; private string? _resourceDirectory; @@ -81,20 +77,12 @@ public sealed partial class FirstRunSetupWindow : Window 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) + private void OnInstallClicked(object sender, RoutedEventArgs e) { if (_setupRunning) { @@ -124,52 +112,22 @@ public sealed partial class FirstRunSetupWindow : Window } SetSetupRunning(true); - SetupStatusBar.Title = "설정 중"; + SetupStatusBar.Title = "경로 저장 중"; SetupStatusBar.Message = - "선택한 경로와 DB 설정·K3D를 적용하고 있습니다. 완료될 때까지 Visual Studio 실행을 중지하지 마세요."; + "선택한 Cuts/Res 폴더 경로를 저장하고 있습니다."; SetupStatusBar.Severity = InfoBarSeverity.Informational; try { - var result = await RunSetupAsync( + SaveSelectedRuntimePaths( 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; - } + _cutsDirectory!, + _resourceDirectory!); SetupStatusBar.Title = "설정 완료"; SetupStatusBar.Message = - "이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요. Development Live로 실행됩니다."; + "폴더 경로를 저장했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요."; SetupStatusBar.Severity = InfoBarSeverity.Success; InstallButton.Visibility = Visibility.Collapsed; CloseButton.Visibility = Visibility.Visible; @@ -177,7 +135,7 @@ public sealed partial class FirstRunSetupWindow : Window catch { ShowError( - "첫 실행 설정을 시작하지 못했습니다. Visual Studio와 Windows PowerShell 설치 상태를 확인해 주세요."); + "폴더 경로를 저장하지 못했습니다. 폴더와 Git 소스 위치를 확인해 주세요."); } finally { @@ -313,20 +271,6 @@ public sealed partial class FirstRunSetupWindow : Window 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 @@ -343,15 +287,12 @@ public sealed partial class FirstRunSetupWindow : Window 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)) + if (!File.Exists(project)) { throw new InvalidOperationException( "The source repository metadata is not usable."); @@ -360,161 +301,119 @@ public sealed partial class FirstRunSetupWindow : Window return repositoryRoot; } - private static async Task RunSetupAsync( + private static void SaveSelectedRuntimePaths( string repositoryRoot, string runtimeRoot, - string resourceDirectory, - bool replaceExistingLocalConfiguration) + string cutsDirectory, + string resourceDirectory) { - var setupScript = Path.Combine( - repositoryRoot, - SetupScriptRelativePath); - var startInfo = new ProcessStartInfo + var store = new LegacyOperatorSettingsStore(); + var loaded = store.Load(); + var current = loaded.Failure == LegacyOperatorSettingsStoreFailure.None + ? loaded.Settings + : LegacyOperatorSettings.Default; + var saved = store.Save(current with { - 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 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)) + SceneDirectory = cutsDirectory, + ResourceDirectory = resourceDirectory + }); + if (!saved.Succeeded) { throw new InvalidOperationException( - "The Windows system directory is unavailable."); + "The selected runtime folders could not be saved."); } - 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; + SaveRuntimeBinding(repositoryRoot, runtimeRoot); } - private static async Task ReadBoundedTailAsync(StreamReader reader) + private static void SaveRuntimeBinding( + string repositoryRoot, + string runtimeRoot) { - var tail = new StringBuilder(MaximumDiagnosticCharacters); - var buffer = new char[4096]; - int count; - while ((count = await reader.ReadAsync(buffer)) > 0) + var destination = Path.Combine( + repositoryRoot, + LocalBuildPropsFileName); + if (Directory.Exists(destination)) { - tail.Append(buffer, 0, count); - if (tail.Length > MaximumDiagnosticCharacters) + throw new InvalidOperationException( + "The local runtime binding path is not a file."); + } + + if (File.Exists(destination)) + { + var attributes = File.GetAttributes(destination); + if ((attributes & (FileAttributes.Directory | + FileAttributes.ReparsePoint | + FileAttributes.Device)) != 0) { - tail.Remove( - 0, - tail.Length - MaximumDiagnosticCharacters); + throw new InvalidOperationException( + "The local runtime binding file is unsafe."); } } - return tail.ToString(); + var bytes = CreateRuntimeBinding(runtimeRoot); + var temporary = Path.Combine( + repositoryRoot, + $".{LocalBuildPropsFileName}.{Guid.NewGuid():N}.tmp"); + try + { + using (var stream = new FileStream( + temporary, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + bufferSize: 4096, + FileOptions.WriteThrough)) + { + stream.Write(bytes); + stream.Flush(flushToDisk: true); + } + + File.Move(temporary, destination, overwrite: true); + temporary = string.Empty; + if (!File.ReadAllBytes(destination).AsSpan().SequenceEqual(bytes)) + { + throw new IOException( + "The local runtime binding failed verification."); + } + } + finally + { + if (!string.IsNullOrEmpty(temporary) && + File.Exists(temporary)) + { + File.Delete(temporary); + } + } } - private static string? LastDiagnosticLine(string diagnostic) + private static byte[] CreateRuntimeBinding(string runtimeRoot) { - var line = diagnostic - .Split( - ['\r', '\n'], - StringSplitOptions.RemoveEmptyEntries | - StringSplitOptions.TrimEntries) - .LastOrDefault(); - if (string.IsNullOrWhiteSpace(line)) + using var stream = new MemoryStream(capacity: 512); + var settings = new XmlWriterSettings { - return null; + Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), + Indent = true, + NewLineChars = Environment.NewLine, + NewLineHandling = NewLineHandling.Replace + }; + using (var writer = XmlWriter.Create(stream, settings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement("Project"); + writer.WriteStartElement("PropertyGroup"); + writer.WriteElementString( + "LegacyRuntimeSourceRoot", + runtimeRoot); + writer.WriteElementString( + "LegacyRuntimeAssetsMode", + "Required"); + writer.WriteEndElement(); + writer.WriteEndElement(); + writer.WriteEndDocument(); } - return line.Length <= 400 ? line : string.Concat(line[..397], "..."); + return stream.ToArray(); } private void SetSetupRunning(bool running) @@ -551,8 +450,4 @@ public sealed partial class FirstRunSetupWindow : Window return AppWindow.GetFromWindowId(windowId); } - private sealed record FirstRunSetupResult( - bool Succeeded, - string? Diagnostic, - bool LocalConfigurationReplacementRequired); } diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj index ee5a172..df8ea79 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj @@ -26,8 +26,8 @@ 0.1.0 $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))