403 lines
22 KiB
PowerShell
403 lines
22 KiB
PowerShell
#Requires -Version 5.1
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $SpecificationPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string] $OutputPath
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$utf8NoBom = [Text.UTF8Encoding]::new($false, $true)
|
|
$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
$orchestratorPath = Join-Path $PSScriptRoot 'Invoke-LegacyPgmPrepareGate.ps1'
|
|
$nodeHelperPath = Join-Path $PSScriptRoot 'Test-LegacyPgmPrepareGate.mjs'
|
|
$userProfilePath = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
|
|
$approvedNodePath = [IO.Path]::GetFullPath((Join-Path $userProfilePath '.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'))
|
|
$expectedRemote = 'https://gitea.comtropy.synology.me/Wickedness/MBN_STOCK_WEBVIEW.git'
|
|
$expectedUrl = 'https://legacy-parity.mbn.local/index.html'
|
|
$approvedSceneDirectory = 'C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts'
|
|
$approvedCutPath = Join-Path $approvedSceneDirectory '5001.t2s'
|
|
$approvedCutSha256 = '99CE3B689A42D8C42BEB09A86FA10C2D7C1AEF4F50D324D81276C1A1E4C4D8A7'
|
|
$approvedMsixPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\AppPackages\MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64_Test\MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64.msix'
|
|
$approvedMsixSha256 = '0E44F87FDD31852B7DDFCD6CF3A6325DA674F1F5A23AD39CB262A725AEB84367'
|
|
$approvedDatabaseConfigPath = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)) 'MBN_STOCK_WEBVIEW\Config\database.local.json'
|
|
$expectedMarketText = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('7L2U7Iqk7ZS8'))
|
|
$expectedStockName = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('7IK87ISx7KCE7J6Q'))
|
|
$expectedCutLabel = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('MeyXtO2MkOq4sOuzuF/tmITsnqzqsIA='))
|
|
$expectedGraphicType = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('MeyXtO2MkOq4sOuzuA=='))
|
|
$expectedSubtype = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('7ZiE7J6s6rCA'))
|
|
|
|
function Fail([string] $Message) {
|
|
throw [InvalidOperationException]::new($Message)
|
|
}
|
|
|
|
function FullPath([string] $Path, [string] $Name) {
|
|
if ([string]::IsNullOrWhiteSpace($Path) -or -not [IO.Path]::IsPathRooted($Path)) {
|
|
Fail "$Name must be an absolute path."
|
|
}
|
|
return [IO.Path]::GetFullPath($Path)
|
|
}
|
|
|
|
function SamePath([string] $Left, [string] $Right) {
|
|
return [string]::Equals(
|
|
[IO.Path]::GetFullPath($Left).TrimEnd('\'),
|
|
[IO.Path]::GetFullPath($Right).TrimEnd('\'),
|
|
[StringComparison]::OrdinalIgnoreCase)
|
|
}
|
|
|
|
function Assert-NoReparse([string] $Path, [string] $Name) {
|
|
$current = [IO.Path]::GetFullPath($Path)
|
|
while (-not [string]::IsNullOrWhiteSpace($current)) {
|
|
if ([IO.File]::Exists($current) -or [IO.Directory]::Exists($current)) {
|
|
if (([IO.File]::GetAttributes($current) -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
Fail "$Name contains a reparse point."
|
|
}
|
|
}
|
|
$parent = [IO.Directory]::GetParent($current)
|
|
if ($null -eq $parent) { break }
|
|
$current = $parent.FullName
|
|
}
|
|
}
|
|
|
|
function Read-Json([string] $Path, [string] $Name) {
|
|
$full = FullPath $Path $Name
|
|
if (-not [IO.File]::Exists($full)) { Fail "$Name does not exist." }
|
|
Assert-NoReparse $full $Name
|
|
$bytes = [IO.File]::ReadAllBytes($full)
|
|
try {
|
|
if ($bytes.Length -eq 0 -or $bytes.Length -gt 1MB) { Fail "$Name has an unsafe size." }
|
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
|
Fail "$Name must be UTF-8 without BOM."
|
|
}
|
|
try { $text = $utf8NoBom.GetString($bytes) }
|
|
catch { Fail "$Name is not strict UTF-8." }
|
|
try { return $text | ConvertFrom-Json -ErrorAction Stop }
|
|
catch { Fail "$Name is not valid JSON." }
|
|
}
|
|
finally { [Array]::Clear($bytes, 0, $bytes.Length) }
|
|
}
|
|
|
|
function Require([object] $Object, [string] $Name, [string] $Context) {
|
|
if ($null -eq $Object -or -not ($Object.PSObject.Properties.Name -ccontains $Name)) {
|
|
Fail "$Context.$Name is required."
|
|
}
|
|
return $Object.$Name
|
|
}
|
|
|
|
function Assert-ExactProperties([object] $Object, [string[]] $Expected, [string] $Context) {
|
|
if ($null -eq $Object) { Fail "$Context is required." }
|
|
$actual = @($Object.PSObject.Properties.Name)
|
|
if ($actual.Count -ne $Expected.Count) { Fail "$Context property count is not exact." }
|
|
foreach ($name in $Expected) {
|
|
if (@($actual | Where-Object { $_ -ceq $name }).Count -ne 1) {
|
|
Fail "$Context property set is not exact: $name."
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-JsonInt([object] $Value, [int] $Expected, [string] $Name) {
|
|
if ($Value -isnot [int] -or [int]$Value -ne $Expected) { Fail "$Name must be JSON integer $Expected." }
|
|
}
|
|
|
|
function Assert-JsonBool([object] $Value, [bool] $Expected, [string] $Name) {
|
|
if ($Value -isnot [bool] -or [bool]$Value -ne $Expected) { Fail "$Name must be JSON boolean $Expected." }
|
|
}
|
|
|
|
function Assert-JsonString([object] $Value, [string] $Expected, [string] $Name) {
|
|
if ($Value -isnot [string] -or [string]$Value -cne $Expected) { Fail "$Name must be the exact JSON string." }
|
|
}
|
|
|
|
function New-FilePin([string] $Path, [string] $Name) {
|
|
$full = FullPath $Path $Name
|
|
if (-not [IO.File]::Exists($full)) { Fail "$Name does not exist." }
|
|
Assert-NoReparse $full $Name
|
|
$item = Get-Item -LiteralPath $full -Force
|
|
return [ordered]@{
|
|
path = $full
|
|
length = [int64]$item.Length
|
|
sha256 = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash
|
|
}
|
|
}
|
|
|
|
function Get-RuntimeTreePin([string] $Root) {
|
|
$fullRoot = FullPath $Root 'package.installLocation'
|
|
if (-not [IO.Directory]::Exists($fullRoot)) { Fail 'package.installLocation does not exist.' }
|
|
Assert-NoReparse $fullRoot 'package.installLocation'
|
|
foreach ($directory in [IO.Directory]::GetDirectories($fullRoot, '*', [IO.SearchOption]::AllDirectories)) {
|
|
Assert-NoReparse $directory 'package runtime directory'
|
|
}
|
|
$files = @([IO.Directory]::GetFiles($fullRoot, '*', [IO.SearchOption]::AllDirectories))
|
|
if ($files.Count -eq 0 -or $files.Count -gt 4096) { Fail 'The package runtime tree count is unsafe.' }
|
|
$relative = New-Object 'Collections.Generic.List[string]'
|
|
$rootPrefix = $fullRoot.TrimEnd('\') + '\'
|
|
foreach ($file in $files) {
|
|
Assert-NoReparse $file 'package runtime file'
|
|
if (-not $file.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
Fail 'A package runtime file escaped the install root.'
|
|
}
|
|
$name = $file.Substring($rootPrefix.Length).Replace('\', '/')
|
|
if ($name.Contains('|') -or $name.Contains("`r") -or $name.Contains("`n")) {
|
|
Fail 'A package runtime relative path cannot be represented safely.'
|
|
}
|
|
[void]$relative.Add($name)
|
|
}
|
|
$names = $relative.ToArray()
|
|
[Array]::Sort($names, [StringComparer]::Ordinal)
|
|
$builder = [Text.StringBuilder]::new()
|
|
foreach ($name in $names) {
|
|
$file = Join-Path $fullRoot $name.Replace('/', '\')
|
|
$item = Get-Item -LiteralPath $file -Force
|
|
$hash = (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash
|
|
[void]$builder.Append($name).Append('|').Append(([int64]$item.Length).ToString([Globalization.CultureInfo]::InvariantCulture)).Append('|').Append($hash).Append("`n")
|
|
}
|
|
$manifestBytes = $utf8NoBom.GetBytes($builder.ToString())
|
|
try {
|
|
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
try { $digest = ([BitConverter]::ToString($algorithm.ComputeHash($manifestBytes))).Replace('-', '') }
|
|
finally { $algorithm.Dispose() }
|
|
}
|
|
finally { [Array]::Clear($manifestBytes, 0, $manifestBytes.Length) }
|
|
return [ordered]@{ fileCount = $names.Length; manifestSha256 = $digest }
|
|
}
|
|
|
|
function Assert-GateConfig([string] $Path, [string] $SceneDirectory) {
|
|
$config = Read-Json $Path 'configuration.gate'
|
|
$names = @(
|
|
'mode','host','port','tcpMode','clientPort','outputChannel','layoutIndex',
|
|
'legacySceneFadeDuration','legacySceneBackgroundKind',
|
|
'legacySceneBackgroundAssetPath','legacySceneBackgroundVideoLoopCount',
|
|
'legacySceneBackgroundVideoLoopInfinite','legacyBackgroundDirectory',
|
|
'sceneDirectory','testProcessWindowTitlePattern','testSceneAllowlist',
|
|
'trustedLiveOutputEnabled','queueCapacity','connectTimeoutMilliseconds',
|
|
'operationTimeoutMilliseconds','disconnectTimeoutMilliseconds',
|
|
'processPollIntervalMilliseconds','reconnectDelayMilliseconds',
|
|
'maximumReconnectAttempts','reconnectEnabled',
|
|
'maximumAutomaticRefreshesPerTakeIn')
|
|
Assert-ExactProperties $config $names 'configuration.gate JSON'
|
|
if ($config.testSceneAllowlist -isnot [Array]) {
|
|
Fail 'configuration.gate.testSceneAllowlist must be a JSON array.'
|
|
}
|
|
$allowlist = @(Require $config 'testSceneAllowlist' 'configuration.gate JSON')
|
|
Assert-JsonString $config.mode 'DryRun' 'configuration.gate.mode'
|
|
Assert-JsonString $config.host '127.0.0.1' 'configuration.gate.host'
|
|
Assert-JsonInt $config.port 30001 'configuration.gate.port'
|
|
Assert-JsonInt $config.tcpMode 1 'configuration.gate.tcpMode'
|
|
Assert-JsonInt $config.clientPort 0 'configuration.gate.clientPort'
|
|
Assert-JsonInt $config.layoutIndex 10 'configuration.gate.layoutIndex'
|
|
Assert-JsonInt $config.legacySceneFadeDuration 6 'configuration.gate.legacySceneFadeDuration'
|
|
Assert-JsonString $config.legacySceneBackgroundKind 'None' 'configuration.gate.legacySceneBackgroundKind'
|
|
Assert-JsonInt $config.legacySceneBackgroundVideoLoopCount 2004 'configuration.gate.legacySceneBackgroundVideoLoopCount'
|
|
Assert-JsonBool $config.legacySceneBackgroundVideoLoopInfinite $true 'configuration.gate.legacySceneBackgroundVideoLoopInfinite'
|
|
Assert-JsonBool $config.trustedLiveOutputEnabled $true 'configuration.gate.trustedLiveOutputEnabled'
|
|
Assert-JsonInt $config.queueCapacity 64 'configuration.gate.queueCapacity'
|
|
Assert-JsonInt $config.connectTimeoutMilliseconds 5000 'configuration.gate.connectTimeoutMilliseconds'
|
|
Assert-JsonInt $config.operationTimeoutMilliseconds 5000 'configuration.gate.operationTimeoutMilliseconds'
|
|
Assert-JsonInt $config.disconnectTimeoutMilliseconds 3000 'configuration.gate.disconnectTimeoutMilliseconds'
|
|
Assert-JsonInt $config.processPollIntervalMilliseconds 1000 'configuration.gate.processPollIntervalMilliseconds'
|
|
Assert-JsonInt $config.reconnectDelayMilliseconds 1000 'configuration.gate.reconnectDelayMilliseconds'
|
|
Assert-JsonInt $config.maximumReconnectAttempts 0 'configuration.gate.maximumReconnectAttempts'
|
|
Assert-JsonBool $config.reconnectEnabled $false 'configuration.gate.reconnectEnabled'
|
|
Assert-JsonInt $config.maximumAutomaticRefreshesPerTakeIn 0 'configuration.gate.maximumAutomaticRefreshesPerTakeIn'
|
|
if (
|
|
$null -ne (Require $config 'outputChannel' 'configuration.gate JSON') -or
|
|
$null -ne (Require $config 'legacySceneBackgroundAssetPath' 'configuration.gate JSON') -or
|
|
$null -ne (Require $config 'legacyBackgroundDirectory' 'configuration.gate JSON') -or
|
|
-not (SamePath ([string](Require $config 'sceneDirectory' 'configuration.gate JSON')) $SceneDirectory) -or
|
|
$null -ne (Require $config 'testProcessWindowTitlePattern' 'configuration.gate JSON') -or
|
|
$allowlist.Count -ne 1 -or $allowlist[0] -isnot [string] -or [string]$allowlist[0] -cne '5001') {
|
|
Fail 'configuration.gate JSON is not the closed 5001-only persistent-DryRun configuration.'
|
|
}
|
|
}
|
|
|
|
$specificationPath = FullPath $SpecificationPath 'SpecificationPath'
|
|
$outputPath = FullPath $OutputPath 'OutputPath'
|
|
if ($outputPath.StartsWith($repositoryRoot.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
|
Fail 'OutputPath must remain outside the Git repository so the execution checkout can be clean.'
|
|
}
|
|
if ([IO.File]::Exists($outputPath) -or [IO.Directory]::Exists($outputPath)) {
|
|
Fail 'OutputPath already exists; a frozen plan is never overwritten.'
|
|
}
|
|
if (SamePath $specificationPath $outputPath) { Fail 'SpecificationPath and OutputPath must differ.' }
|
|
if (-not [IO.File]::Exists($orchestratorPath) -or -not [IO.File]::Exists($nodeHelperPath)) {
|
|
Fail 'The committed Gate orchestrator and Node helper must both exist before sealing a plan.'
|
|
}
|
|
|
|
$spec = Read-Json $specificationPath 'SpecificationPath'
|
|
Assert-ExactProperties $spec @(
|
|
'schemaVersion','roundId','repository','package','automation','configuration',
|
|
'cut','k3d','pgm','webView') 'specification'
|
|
Assert-JsonInt (Require $spec 'schemaVersion' 'specification') 1 'specification.schemaVersion'
|
|
$roundId = [string](Require $spec 'roundId' 'specification')
|
|
if ($roundId -cnotmatch '^MBNWEB-[0-9]{8}-[A-Z0-9][A-Z0-9-]{0,31}$') { Fail 'roundId is invalid.' }
|
|
|
|
$repo = Require $spec 'repository' 'specification'
|
|
Assert-ExactProperties $repo @('path','branch','commit','upstream','remoteUrl') 'repository'
|
|
if (-not (SamePath ([string]$repo.path) $repositoryRoot) -or
|
|
[string]$repo.branch -cne 'main' -or [string]$repo.upstream -cne 'origin/main' -or
|
|
[string]$repo.remoteUrl -cne $expectedRemote -or
|
|
[string]$repo.commit -cnotmatch '^[0-9a-f]{40}$') { Fail 'repository pin is not exact.' }
|
|
|
|
$package = Require $spec 'package' 'specification'
|
|
Assert-ExactProperties $package @(
|
|
'name','familyName','fullName','publisher','version','architecture',
|
|
'applicationId','installLocation','executablePath','msixPath') 'package'
|
|
if ([string]$package.name -cne 'Wickedness.MBNStockWebView.LegacyParity' -or
|
|
[string]$package.familyName -cne 'Wickedness.MBNStockWebView.LegacyParity_qbv3jkvsn3aj0' -or
|
|
[string]$package.fullName -cne 'Wickedness.MBNStockWebView.LegacyParity_0.1.0.0_x64__qbv3jkvsn3aj0' -or
|
|
[string]$package.publisher -cne 'CN=Comtrophy' -or [string]$package.version -cne '0.1.0.0' -or
|
|
[string]$package.architecture -cne 'X64' -or [string]$package.applicationId -cne 'LegacyParityApp') {
|
|
Fail 'package identity is not the exact LegacyParity Release x64 package.'
|
|
}
|
|
$installLocation = FullPath ([string]$package.installLocation) 'package.installLocation'
|
|
$expectedInstallLocation = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Release\net8.0-windows10.0.19041.0\win-x64'
|
|
if (-not (SamePath $installLocation $expectedInstallLocation)) {
|
|
Fail 'package.installLocation is not the exact Release x64 output.'
|
|
}
|
|
$executablePath = FullPath ([string]$package.executablePath) 'package.executablePath'
|
|
if (-not (SamePath $executablePath (Join-Path $installLocation 'MBN_STOCK_WEBVIEW.LegacyParityApp.exe'))) {
|
|
Fail 'package executable is outside the exact install root.'
|
|
}
|
|
$msixPath = FullPath ([string]$package.msixPath) 'package.msixPath'
|
|
if (-not (SamePath $msixPath $approvedMsixPath)) {
|
|
Fail 'package.msixPath is not the exact Release x64 AppPackages MSIX.'
|
|
}
|
|
if ((Get-FileHash -LiteralPath $msixPath -Algorithm SHA256).Hash -cne $approvedMsixSha256) {
|
|
Fail 'The approved Release x64 MSIX SHA-256 changed.'
|
|
}
|
|
|
|
$automation = Require $spec 'automation' 'specification'
|
|
Assert-ExactProperties $automation @('nodePath') 'automation'
|
|
if (-not (SamePath ([string]$automation.nodePath) $approvedNodePath)) {
|
|
Fail 'automation.nodePath is not the approved bundled Node runtime.'
|
|
}
|
|
$configuration = Require $spec 'configuration' 'specification'
|
|
Assert-ExactProperties $configuration @('originalPath','gatePath','sceneDirectory') 'configuration'
|
|
$originalConfigPath = FullPath ([string]$configuration.originalPath) 'configuration.originalPath'
|
|
$gateConfigPath = FullPath ([string]$configuration.gatePath) 'configuration.gatePath'
|
|
$sceneDirectory = FullPath ([string]$configuration.sceneDirectory) 'configuration.sceneDirectory'
|
|
if (-not (SamePath $sceneDirectory $approvedSceneDirectory)) {
|
|
Fail 'configuration.sceneDirectory is not the approved read-only original Cuts root.'
|
|
}
|
|
if (SamePath $originalConfigPath $gateConfigPath) { Fail 'Original and Gate config paths must differ.' }
|
|
Assert-GateConfig $gateConfigPath $sceneDirectory
|
|
|
|
$cut = Require $spec 'cut' 'specification'
|
|
Assert-ExactProperties $cut @('path') 'cut'
|
|
$cutPath = FullPath ([string]$cut.path) 'cut.path'
|
|
if (-not (SamePath $cutPath $approvedCutPath)) { Fail 'cut.path is not the approved read-only original 5001.t2s.' }
|
|
if ((Get-FileHash -LiteralPath $cutPath -Algorithm SHA256).Hash -cne $approvedCutSha256) {
|
|
Fail 'The approved original 5001.t2s SHA-256 changed.'
|
|
}
|
|
|
|
$k3d = Require $spec 'k3d' 'specification'
|
|
Assert-ExactProperties $k3d @('nativePath','interopPath') 'k3d'
|
|
$pgm = Require $spec 'pgm' 'specification'
|
|
Assert-ExactProperties $pgm @(
|
|
'processId','startTimeUtc','processName','executablePath','windowTitle',
|
|
'listenerAddress','host','port') 'pgm'
|
|
if ($pgm.processId -isnot [int] -or [int]$pgm.processId -le 0 -or $pgm.port -isnot [int] -or [string]$pgm.startTimeUtc -cnotmatch '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z$' -or
|
|
[string]$pgm.windowTitle -cne 'PGM' -or [string]$pgm.listenerAddress -cne '0.0.0.0' -or
|
|
[string]$pgm.host -cne '127.0.0.1' -or [int]$pgm.port -ne 30001) { Fail 'PGM pin is not the exact current PGM listener contract.' }
|
|
|
|
$webView = Require $spec 'webView' 'specification'
|
|
Assert-ExactProperties $webView @('address','port','url') 'webView'
|
|
if ($webView.port -isnot [int] -or [string]$webView.address -cne '127.0.0.1' -or [int]$webView.port -lt 1024 -or [int]$webView.port -gt 65535 -or
|
|
[int]$webView.port -eq 30001 -or [string]$webView.url -cne $expectedUrl) { Fail 'WebView pin is invalid.' }
|
|
|
|
$plan = [ordered]@{
|
|
schemaVersion = 1
|
|
kind = 'LegacyPgmPrepareGatePlan'
|
|
roundId = $roundId
|
|
createdAtUtc = [DateTime]::UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'", [Globalization.CultureInfo]::InvariantCulture)
|
|
repository = [ordered]@{
|
|
path = $repositoryRoot; branch = 'main'; commit = [string]$repo.commit
|
|
upstream = 'origin/main'; remoteUrl = $expectedRemote
|
|
}
|
|
package = [ordered]@{
|
|
name = [string]$package.name; familyName = [string]$package.familyName
|
|
fullName = [string]$package.fullName; publisher = [string]$package.publisher
|
|
version = [string]$package.version; architecture = 'X64'
|
|
applicationId = [string]$package.applicationId; installLocation = $installLocation
|
|
executable = New-FilePin $executablePath 'package executable'
|
|
msix = New-FilePin $msixPath 'package MSIX'
|
|
runtimeTree = Get-RuntimeTreePin $installLocation
|
|
}
|
|
automation = [ordered]@{
|
|
orchestrator = New-FilePin $orchestratorPath 'Gate orchestrator'
|
|
node = New-FilePin ([string]$automation.nodePath) 'Node runtime'
|
|
helper = New-FilePin $nodeHelperPath 'Node Gate helper'
|
|
}
|
|
configuration = [ordered]@{
|
|
original = New-FilePin $originalConfigPath 'original DryRun config'
|
|
gate = New-FilePin $gateConfigPath 'Gate DryRun config'
|
|
database = New-FilePin $approvedDatabaseConfigPath 'database config'
|
|
sceneDirectory = $sceneDirectory
|
|
}
|
|
cut = [ordered]@{
|
|
sceneCode = '5001'; pageNumberOneBased = 1
|
|
file = New-FilePin $cutPath '5001 cut'
|
|
}
|
|
selector = [ordered]@{
|
|
marketText = $expectedMarketText; groupCode = 'KOSPI'; stockName = $expectedStockName
|
|
stockCode = '005930'; cutLabel = $expectedCutLabel
|
|
graphicType = $expectedGraphicType; subtype = $expectedSubtype
|
|
normalizedSubtype = 'CURRENT'; sceneAlias = '5001'
|
|
}
|
|
k3d = [ordered]@{
|
|
native = New-FilePin ([string]$k3d.nativePath) 'K3D native binary'
|
|
interop = New-FilePin ([string]$k3d.interopPath) 'K3D interop assembly'
|
|
}
|
|
pgm = [ordered]@{
|
|
processId = [int]$pgm.processId; startTimeUtc = [string]$pgm.startTimeUtc
|
|
processName = [string]$pgm.processName
|
|
executable = New-FilePin ([string]$pgm.executablePath) 'PGM executable'
|
|
windowTitle = 'PGM'; listenerAddress = '0.0.0.0'; host = '127.0.0.1'; port = 30001
|
|
}
|
|
webView = [ordered]@{ address = '127.0.0.1'; port = [int]$webView.port; url = $expectedUrl }
|
|
approvalPolicy = [ordered]@{
|
|
requiresSeparateAuthorization = $true; maximumAgeSeconds = 900; maximumClockSkewSeconds = 5
|
|
}
|
|
budgets = [ordered]@{
|
|
connect = 1; prepare = 1; takeIn = 0; next = 0; pageNext = 0
|
|
takeOut = 0; databaseWrite = 0; appClose = 0; retry = 0
|
|
}
|
|
safety = [ordered]@{
|
|
automaticConnectOnly = $true; nodeHelperInvocationMaximum = 1
|
|
nodeHelperFailStopRequired = $true
|
|
packageWrapperFailStopRequired = $true
|
|
persistentGateConfigModeDryRun = $true; restoreOriginalBeforePrepare = $true
|
|
nativeOneShotPrepareGateRequired = $true
|
|
leaveApplicationAndSessionUntouched = $true; forceCloseForbidden = $true
|
|
automaticSessionCleanupForbiddenAfterLaunch = $true; retryForbidden = $true
|
|
}
|
|
}
|
|
|
|
$parent = [IO.Path]::GetDirectoryName($outputPath)
|
|
if ([string]::IsNullOrWhiteSpace($parent)) { Fail 'OutputPath parent is invalid.' }
|
|
[void][IO.Directory]::CreateDirectory($parent)
|
|
Assert-NoReparse $parent 'OutputPath parent'
|
|
$json = ($plan | ConvertTo-Json -Depth 12) + [Environment]::NewLine
|
|
$bytes = $utf8NoBom.GetBytes($json)
|
|
try {
|
|
$stream = [IO.File]::Open($outputPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::Read)
|
|
try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) }
|
|
finally { $stream.Dispose() }
|
|
}
|
|
finally { [Array]::Clear($bytes, 0, $bytes.Length) }
|
|
|
|
[pscustomobject][ordered]@{
|
|
result = 'PLAN_CREATED_COMMAND_FREE'
|
|
roundId = $roundId
|
|
planPath = $outputPath
|
|
planLength = (Get-Item -LiteralPath $outputPath).Length
|
|
planSha256 = (Get-FileHash -LiteralPath $outputPath -Algorithm SHA256).Hash
|
|
authorizationCreated = $false
|
|
appCommandsIssued = 0
|
|
vendorCommandsIssued = 0
|
|
}
|