92 lines
8.7 KiB
PowerShell
92 lines
8.7 KiB
PowerShell
#Requires -Version 7.0
|
|
# Prepared only. Run in a persistent shell; this observer waits for normal close.
|
|
[CmdletBinding()]
|
|
param([ValidateRange(1,99)][int]$Run=1)
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'verify.ps1')
|
|
$nativeRoot = $PSScriptRoot
|
|
$nativePackage = Join-Path $nativeRoot 'package'
|
|
$nativeProfile = Join-Path $nativeRoot 'profile'
|
|
$nativeCwd = Join-Path $nativeRoot 'unrelated-cwd'
|
|
$runRoot = Join-Path $nativeRoot ('runs/run-{0:d2}' -f $Run)
|
|
$nativeManifest = Get-Content -LiteralPath (Join-Path $nativeRoot 'input-manifest.json') -Raw | ConvertFrom-Json
|
|
$nativePin = $nativeManifest.source_commit
|
|
$nativeInputCount = [int]$nativeManifest.verified_input_count
|
|
if ($nativePin -cne 'b10b28cefd1d0794073ba241a0c8de9a83e85f68' -or $nativeInputCount -ne 612) { throw 'Invalid staged identity.' }
|
|
$staging = Get-Content -LiteralPath (Join-Path $nativeRoot 'staging-record.json') -Raw | ConvertFrom-Json
|
|
$stagingHash=Get-NativeHash (Join-Path $nativeRoot 'staging-record.json')
|
|
if ($staging.source_commit -cne $nativePin -or $staging.manifest_sha256 -cne (Get-NativeHash (Join-Path $nativeRoot 'input-manifest.json'))) { throw 'Final staged manifest changed.' }
|
|
if ($nativeManifest.source_commit -cne $nativePin -or $nativeManifest.verified_input_count -ne $nativeInputCount -or -not $nativeManifest.before_after_input_files_unchanged) { throw 'Wrong staged source binding.' }
|
|
Assert-NativeFiles $nativeRoot $nativeManifest.immutable_files
|
|
$previousCloseHash=$null
|
|
if ($Run -eq 1) {
|
|
Assert-NativeFiles $nativeRoot $nativeManifest.fixture_files
|
|
if (Test-Path -LiteralPath (Join-Path $nativeRoot 'runs')) { throw 'First run requires fresh run evidence.' }
|
|
} else {
|
|
$previousRoot=Join-Path $nativeRoot ('runs/run-{0:d2}' -f ($Run-1))
|
|
$previousClosePath=Join-Path $previousRoot 'normal-close.json'
|
|
$previousClose=Read-NativeJson $previousClosePath
|
|
$previousLaunch=Read-NativeJson (Join-Path $previousRoot 'launch.json')
|
|
if ($previousClose.source_commit -cne $nativePin -or $previousLaunch.source_commit -cne $nativePin -or $previousClose.exit_code -ne 0 -or -not $previousClose.immutable_inputs_unchanged -or $previousClose.launch_sha256 -cne (Get-NativeHash (Join-Path $previousRoot 'launch.json')) -or $previousLaunch.manifest_sha256 -cne (Get-NativeHash (Join-Path $nativeRoot 'input-manifest.json')) -or $previousClose.pid -ne $previousLaunch.pid -or $previousClose.started_utc -cne $previousLaunch.started_utc) { throw 'Previous successful bound close required.' }
|
|
if ((Get-NativeHash (Join-Path $previousRoot 'logs/studio.stdout.log')) -cne $previousClose.stdout_sha256 -or (Get-NativeHash (Join-Path $previousRoot 'logs/studio.stderr.log')) -cne $previousClose.stderr_sha256) { throw 'Previous logs changed.' }
|
|
Assert-NativeFiles $nativeRoot $previousClose.working_files
|
|
$previousCloseHash=Get-NativeHash $previousClosePath
|
|
}
|
|
$build = Assert-BuildEvidence (Join-Path $nativeRoot 'evidence')
|
|
if ($build.binary.sha256 -cne $nativeManifest.binary.sha256 -or (Get-NativeHash (Join-Path $nativePackage 'arcadia-studio.exe')) -cne $build.binary.sha256 -or (Get-Item -LiteralPath (Join-Path $nativePackage 'arcadia-studio.exe')).Length -ne $build.binary.bytes) { throw 'Wrong staged binary.' }
|
|
if ((Get-NativeHash (Join-Path $nativePackage 'libunwind.dll')) -cne 'aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a') { throw 'Wrong runtime.' }
|
|
foreach ($directory in @($nativeProfile,$nativeCwd)) {
|
|
if ($Run -eq 1 -and (Test-Path -LiteralPath $directory)) { throw "Fresh launch directory required: $directory" }; if ($Run -gt 1 -and -not (Test-Path -LiteralPath $directory -PathType Container)) { throw "Previous private launch directory required: $directory" }
|
|
}
|
|
if (Test-Path -LiteralPath $runRoot) { throw 'Preserve existing run evidence.' }
|
|
foreach ($name in @('launch.json','normal-close.json','launch-failure.json','logs')) {
|
|
if (Test-Path -LiteralPath (Join-Path $runRoot $name)) { throw "Refusing to overwrite launch evidence: $name" }
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($env:SystemRoot) -or -not (Test-Path -LiteralPath (Join-Path $env:SystemRoot 'System32') -PathType Container)) { throw 'Normal Windows SystemRoot required.' }
|
|
$nativePath = $nativePackage + ';' + (Join-Path $env:SystemRoot 'System32') + ';' + $env:SystemRoot + ';' + (Join-Path $env:SystemRoot 'System32/Wbem')
|
|
$nativeOut = Join-Path $runRoot 'logs/studio.stdout.log'
|
|
$nativeErr = Join-Path $runRoot 'logs/studio.stderr.log'
|
|
$workingNames=@('incident-workspace.arcadia','service-details.arcadia')
|
|
$state = [ordered]@{working_files_before=@(foreach($name in $workingNames){Get-NativeRecord $nativeRoot (Join-Path $nativeRoot ('working/'+$name))});fixture_entries=@($workingNames|ForEach-Object {Join-Path $nativeRoot ('working/'+$_)});run=$Run;previous_close_sha256=$previousCloseHash;working_file_before=(Get-NativeRecord $nativeRoot (Join-Path $nativeRoot 'working/incident-workspace.arcadia'));source_commit=$nativePin;manifest_sha256=(Get-NativeHash (Join-Path $nativeRoot 'input-manifest.json'));script_sha256=(Get-NativeHash $PSCommandPath);executable=(Join-Path $nativePackage 'arcadia-studio.exe');binary_sha256=(Get-NativeHash (Join-Path $nativePackage 'arcadia-studio.exe'));localappdata=$nativeProfile;working_directory=$nativeCwd;child_path=$nativePath;arguments=@();environment_scope='Child ProcessStartInfo only';fixture_entry=(Join-Path $nativeRoot 'working/incident-workspace.arcadia');pid=$null;started_utc=$null}
|
|
$process = $null; $outStream = $null; $errStream = $null
|
|
try {
|
|
foreach ($directory in @($nativeProfile,$nativeCwd,(Join-Path $runRoot 'logs'))) { [void][IO.Directory]::CreateDirectory($directory) }
|
|
$outStream = [IO.File]::Open($nativeOut,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
|
$errStream = [IO.File]::Open($nativeErr,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
|
$info = [Diagnostics.ProcessStartInfo]::new()
|
|
$info.FileName = $state.executable
|
|
$info.WorkingDirectory = $nativeCwd
|
|
$info.UseShellExecute = $false
|
|
$info.CreateNoWindow = $true
|
|
$info.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
|
|
$info.RedirectStandardOutput = $true
|
|
$info.RedirectStandardError = $true
|
|
$info.Environment['LOCALAPPDATA'] = $nativeProfile
|
|
$info.Environment['PATH'] = $nativePath
|
|
$process = [Diagnostics.Process]::new()
|
|
$process.StartInfo = $info
|
|
if (-not $process.Start()) { throw 'Process.Start returned false.' }
|
|
$outCopy = $process.StandardOutput.BaseStream.CopyToAsync($outStream)
|
|
$errCopy = $process.StandardError.BaseStream.CopyToAsync($errStream)
|
|
$state.pid = $process.Id
|
|
$state.started_utc = $process.StartTime.ToUniversalTime().ToString('o')
|
|
Write-NativeJson (Join-Path $runRoot 'launch.json') $state
|
|
$state | ConvertTo-Json -Depth 15 | Write-Output
|
|
$process.WaitForExit()
|
|
[Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($outCopy,$errCopy))
|
|
$outStream.Flush($true); $errStream.Flush($true)
|
|
$outStream.Dispose(); $outStream = $null
|
|
$errStream.Dispose(); $errStream = $null
|
|
Assert-NativeFiles $nativeRoot $nativeManifest.immutable_files
|
|
if ((Get-NativeHash (Join-Path $nativeRoot 'input-manifest.json')) -cne $state.manifest_sha256 -or (Get-NativeHash (Join-Path $nativeRoot 'staging-record.json')) -cne $stagingHash) { throw 'Manifest or staging receipt changed during native run.' }
|
|
Write-NativeJson (Join-Path $runRoot 'normal-close.json') ([ordered]@{run=$Run;launch_sha256=(Get-NativeHash (Join-Path $runRoot 'launch.json'));source_commit=$nativePin;pid=$state.pid;started_utc=$state.started_utc;observed_exit_utc=[DateTime]::UtcNow.ToString('o');exit_code=$process.ExitCode;immutable_inputs_unchanged=$true;baseline_files=@(foreach($name in $workingNames){Get-NativeRecord $nativeRoot (Join-Path $nativeRoot ('baseline/'+$name))});working_files=@(foreach($name in $workingNames){Get-NativeRecord $nativeRoot (Join-Path $nativeRoot ('working/'+$name))});stdout_bytes=(Get-Item -LiteralPath $nativeOut).Length;stderr_bytes=(Get-Item -LiteralPath $nativeErr).Length;stdout_sha256=(Get-NativeHash $nativeOut);stderr_sha256=(Get-NativeHash $nativeErr);scope='Observed bound process exit without forced termination. Close cause and native acceptance require user/root observation; fixture edits are expected and compared separately.'})
|
|
} catch {
|
|
Write-NativeJson (Join-Path $runRoot 'launch-failure.json') ([ordered]@{utc=[DateTime]::UtcNow.ToString('o');pid=$state.pid;error=$_.Exception.Message;owned_process_not_killed=$true})
|
|
throw
|
|
} finally {
|
|
if ($null -ne $outStream) { $outStream.Dispose() }
|
|
if ($null -ne $errStream) { $errStream.Dispose() }
|
|
if ($null -ne $process) { $process.Dispose() }
|
|
}
|