feat: add two-folder first-run playout setup
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user