168 lines
11 KiB
PowerShell
168 lines
11 KiB
PowerShell
#Requires -Version 7.0
|
|
# Prepared only. Root dispatches in a persistent shell after extraction/review.
|
|
# Keeps a bound process/logger until GUI close; never kills the application.
|
|
[CmdletBinding()]
|
|
param()
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
$launchRoot = $PSScriptRoot
|
|
$launchMain = [IO.Path]::GetFullPath((Join-Path $launchRoot '../..'))
|
|
$launchSource = '33e30c1cd7a6c99513a0ebd02ba68fccbad8f58d'
|
|
$launchName = 'ArcadiaStudio-0.2.0-preview.canvas-repair.20260912-x86_64-pc-windows-gnullvm'
|
|
$launchPackage = Join-Path $launchRoot ('extracted/' + $launchName)
|
|
$launchGateRoot = Join-Path $launchMain '.local/repair-union-windows-20260911'
|
|
$launchProfile = Join-Path $launchRoot 'profile-packaged'
|
|
$launchCwd = Join-Path $launchRoot 'unrelated-cwd'
|
|
$launchReceipt = Join-Path $launchRoot 'launch-packaged.json'
|
|
$launchTerminal = Join-Path $launchRoot 'normal-close.json'
|
|
$launchFailure = Join-Path $launchRoot 'launch-failure.json'
|
|
$launchStdout = Join-Path $launchRoot 'logs/studio.stdout.log'
|
|
$launchStderr = Join-Path $launchRoot 'logs/studio.stderr.log'
|
|
$launchUtf8 = [Text.UTF8Encoding]::new($false)
|
|
|
|
function Read-LaunchFile([string]$Path) {
|
|
$item = Get-Item -LiteralPath $Path -Force
|
|
if ($item.PSIsContainer) { throw "Expected file: $Path" }
|
|
$parent = $item
|
|
while ($null -ne $parent) {
|
|
if (($parent.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Reparse input: $($parent.FullName)" }
|
|
$parent = if ($parent -is [IO.FileInfo]) { $parent.Directory } else { $parent.Parent }
|
|
}
|
|
return [ordered]@{path=$item.FullName;bytes=$item.Length;sha256=(Get-FileHash -LiteralPath $item.FullName).Hash.ToLowerInvariant()}
|
|
}
|
|
function Open-LaunchOutput([string]$Path) {
|
|
return [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
|
|
}
|
|
function Write-LaunchRecord($Record, [IO.FileStream]$Stream) {
|
|
$bytes = $launchUtf8.GetBytes(($Record | ConvertTo-Json -Depth 15) + "`n")
|
|
$Stream.Position = 0
|
|
$Stream.SetLength(0)
|
|
$Stream.Write($bytes, 0, $bytes.Length)
|
|
$Stream.Flush($true)
|
|
}
|
|
function Check-PackageFiles($Extraction) {
|
|
$expected = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($file in $Extraction.files) {
|
|
if (-not $file.path.StartsWith($launchName + '/', [StringComparison]::Ordinal) -or $file.path.Contains('\') -or $file.path.Contains(':') -or @($file.path.Split('/') | Where-Object { $_ -eq '..' -or $_ -eq '.' -or $_ -eq '' }).Count) { throw 'Unsafe extraction record path.' }
|
|
$full = [IO.Path]::GetFullPath((Join-Path (Join-Path $launchRoot 'extracted') $file.path))
|
|
if (-not $full.StartsWith($launchPackage + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or -not $expected.Add($full)) { throw 'Duplicate or escaping extraction record.' }
|
|
$actual = Read-LaunchFile $full
|
|
if ($actual.sha256 -cne $file.sha256 -or $actual.bytes -ne $file.bytes) { throw "Package file changed: $($file.path)" }
|
|
}
|
|
$actualPaths = @(Get-ChildItem -LiteralPath $launchPackage -Recurse -File -Force | ForEach-Object FullName)
|
|
if ($actualPaths.Count -ne $expected.Count -or @($actualPaths | Where-Object { -not $expected.Contains($_) }).Count) { throw 'Package inventory changed.' }
|
|
return $expected.Count
|
|
}
|
|
|
|
$extractionFile = Read-LaunchFile (Join-Path $launchRoot 'extraction-readback.json')
|
|
$extraction = Get-Content -LiteralPath $extractionFile.path -Raw | ConvertFrom-Json
|
|
if ($extraction.exit -ne 0 -or $extraction.source_commit -cne $launchSource -or [IO.Path]::GetFullPath($extraction.package_directory) -cne $launchPackage) { throw 'Matching successful extraction is required.' }
|
|
$buildFile = Read-LaunchFile (Join-Path $launchGateRoot 'binary-manifest.json')
|
|
$build = Get-Content -LiteralPath $buildFile.path -Raw | ConvertFrom-Json
|
|
if ($build.source_identity.Split(';')[0] -cne $launchSource -or $build.input_manifest_sha256.ToLowerInvariant() -cne '0f21b9a852cf44699bf29fce0a35ec460744c85f5e26f0ea4920e8568211e312' -or $build.verified_inputs -ne 530 -or $build.verified_input_bytes -ne 9727025) { throw 'Matching preserved build identity is required.' }
|
|
foreach ($name in @('focused','desktop-tests','desktop-clippy','studio-build')) {
|
|
$gate = $build.gate_statuses.$name
|
|
if ($gate.exit -ne 0 -or [string]::IsNullOrWhiteSpace($gate.completed_utc) -or $gate.name -cne "repair-union-windows-20260911/$name") { throw "Incomplete preserved gate: $name" }
|
|
}
|
|
$studio = Read-LaunchFile (Join-Path $launchPackage 'arcadia-studio.exe')
|
|
$runtime = Read-LaunchFile (Join-Path $launchPackage 'libunwind.dll')
|
|
foreach ($pair in @(@('arcadia-studio.exe',$studio), @('libunwind.dll',$runtime))) {
|
|
$matches = @($build.binaries | Where-Object { [IO.Path]::GetFileName($_.copy) -ceq $pair[0] })
|
|
if ($matches.Count -ne 1) { throw 'Missing/duplicate preserved binary record.' }
|
|
$pin = $matches[0]
|
|
$immutable = Read-LaunchFile (Join-Path (Join-Path $launchGateRoot 'bin') $pair[0])
|
|
if ($pair[1].sha256 -cne $pin.copy_sha256.ToLowerInvariant() -or $pair[1].sha256 -cne $immutable.sha256 -or $pair[1].bytes -ne $pin.bytes -or $pair[1].bytes -ne $immutable.bytes) { throw 'Extracted binary differs from immutable build.' }
|
|
}
|
|
if ($runtime.sha256 -cne 'aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a') { throw 'Unexpected runtime.' }
|
|
$packageCount = Check-PackageFiles $extraction
|
|
if ([string]::IsNullOrWhiteSpace($env:SystemRoot) -or -not (Test-Path -LiteralPath (Join-Path $env:SystemRoot 'System32') -PathType Container)) { throw 'Inherited normal Windows SystemRoot is required.' }
|
|
$childPath = (Join-Path $env:SystemRoot 'System32') + ';' + $env:SystemRoot + ';' + $launchPackage
|
|
foreach ($directory in @($launchProfile, $launchCwd)) {
|
|
if (Test-Path -LiteralPath $directory) {
|
|
$item = Get-Item -LiteralPath $directory -Force
|
|
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or @(Get-ChildItem -LiteralPath $directory -Force).Count) { throw "Fresh empty ordinary directory required: $directory" }
|
|
}
|
|
}
|
|
foreach ($output in @($launchReceipt,$launchTerminal,$launchFailure,$launchStdout,$launchStderr)) {
|
|
if (Test-Path -LiteralPath $output) { throw "Never overwrite launch evidence: $output" }
|
|
}
|
|
$state = [ordered]@{
|
|
source_commit=$launchSource;extraction_manifest=$extractionFile;binary_manifest=$buildFile
|
|
package_directory=$launchPackage;verified_package_files=$packageCount;studio=$studio;runtime=$runtime
|
|
launch_script=(Read-LaunchFile $PSCommandPath);process_path=$childPath;isolated_localappdata=$launchProfile
|
|
working_directory=$launchCwd;arguments=@();environment_scope='Child ProcessStartInfo only; normal Windows environment inherited.'
|
|
window_style='Hidden';use_shell_execute=$false;create_no_window=$true
|
|
stdout=$launchStdout;stderr=$launchStderr;pid=$null;process_started_utc=$null
|
|
state='preflight_passed';acceptance='Pending root native observations and independent complete model/source comparisons.'
|
|
}
|
|
$receiptStream = $null; $outStream = $null; $errStream = $null; $process = $null; $started = $false
|
|
try {
|
|
$receiptStream = Open-LaunchOutput $launchReceipt
|
|
Write-LaunchRecord $state $receiptStream
|
|
foreach ($directory in @($launchProfile, $launchCwd, (Join-Path $launchRoot 'logs'))) { [void][IO.Directory]::CreateDirectory($directory) }
|
|
$outStream = Open-LaunchOutput $launchStdout
|
|
$errStream = Open-LaunchOutput $launchStderr
|
|
$info = [Diagnostics.ProcessStartInfo]::new()
|
|
$info.FileName = $studio.path
|
|
$info.WorkingDirectory = $launchCwd
|
|
$info.UseShellExecute = $false
|
|
$info.CreateNoWindow = $true
|
|
$info.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
|
|
$info.RedirectStandardOutput = $true
|
|
$info.RedirectStandardError = $true
|
|
$info.Environment['PATH'] = $childPath
|
|
$info.Environment['LOCALAPPDATA'] = $launchProfile
|
|
foreach ($file in @($studio,$runtime,$extractionFile,$buildFile)) {
|
|
if ((Read-LaunchFile $file.path).sha256 -cne $file.sha256) { throw "Launch input changed: $($file.path)" }
|
|
}
|
|
$process = [Diagnostics.Process]::new()
|
|
$process.StartInfo = $info
|
|
$state['launch_requested_utc'] = [DateTime]::UtcNow.ToString('o')
|
|
if (-not $process.Start()) { throw 'Process.Start returned false.' }
|
|
$started = $true
|
|
$state.pid = $process.Id
|
|
$outCopy = $process.StandardOutput.BaseStream.CopyToAsync($outStream)
|
|
$errCopy = $process.StandardError.BaseStream.CopyToAsync($errStream)
|
|
$state.process_started_utc = $process.StartTime.ToUniversalTime().ToString('o')
|
|
$state.state = 'launched'
|
|
Write-LaunchRecord $state $receiptStream
|
|
$receiptStream.Dispose(); $receiptStream = $null
|
|
$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
|
|
$closed = [ordered]@{
|
|
source_commit=$launchSource;pid=$state.pid;process_started_utc=$state.process_started_utc
|
|
observed_exit_utc=[DateTime]::UtcNow.ToString('o');exit_code=$process.ExitCode
|
|
scope='Bound process termination/raw logs; reason for close and UI acceptance require root observation.'
|
|
stdout=(Read-LaunchFile $launchStdout);stderr=(Read-LaunchFile $launchStderr)
|
|
verified_package_files_after_close=(Check-PackageFiles $extraction)
|
|
package_files_unchanged=$true
|
|
extraction_manifest_unchanged=((Read-LaunchFile $extractionFile.path).sha256 -ceq $extractionFile.sha256)
|
|
binary_manifest_unchanged=((Read-LaunchFile $buildFile.path).sha256 -ceq $buildFile.sha256)
|
|
}
|
|
$terminalStream = Open-LaunchOutput $launchTerminal
|
|
try { Write-LaunchRecord $closed $terminalStream } finally { $terminalStream.Dispose() }
|
|
$closed | ConvertTo-Json -Depth 15 | Write-Output
|
|
} catch {
|
|
$failure = [ordered]@{utc=[DateTime]::UtcNow.ToString('o');pid=$state.pid;process_started_utc=$state.process_started_utc;launched=$started;error=$_.Exception.Message;stack=$_.ScriptStackTrace;owned_process_not_killed=$true}
|
|
if ($started -and $null -ne $process) {
|
|
try {
|
|
$failure['process_has_exited'] = $process.HasExited
|
|
if ($process.HasExited) { $failure['exit_code'] = $process.ExitCode }
|
|
} catch { $failure['exit_readback_error'] = $_.Exception.Message }
|
|
}
|
|
if (-not (Test-Path -LiteralPath $launchFailure)) {
|
|
$failureStream = Open-LaunchOutput $launchFailure
|
|
try { Write-LaunchRecord $failure $failureStream } finally { $failureStream.Dispose() }
|
|
}
|
|
throw
|
|
} finally {
|
|
if ($null -ne $receiptStream) { $receiptStream.Dispose() }
|
|
if ($null -ne $outStream) { $outStream.Dispose() }
|
|
if ($null -ne $errStream) { $errStream.Dispose() }
|
|
if ($null -ne $process) { $process.Dispose() }
|
|
}
|