184 lines
9.8 KiB
PowerShell
184 lines
9.8 KiB
PowerShell
#Requires -Version 7.0
|
|
# PREPARED ONLY. Root decides when to invoke. Keep this logger alive until normal GUI close.
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
$nativeRoot = Join-Path $PSScriptRoot 'native-a57c334-camera'
|
|
$nativeFreeze = 'a57c334b302eb13352b4bb65df1c75d94d002348'
|
|
$nativeManifestHash = '293892C2F430D6EADB58C6678F3C1B346742B3AA4D2FE5CE3729F896EAD2C2DB'
|
|
$nativeInputHash = '47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3'
|
|
$nativeGuiHash = '69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40'
|
|
$nativeDllHash = 'AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A'
|
|
$nativeBin = Join-Path $nativeRoot 'bin'
|
|
$nativeProfile = Join-Path $nativeRoot 'profile'
|
|
$nativeCwd = Join-Path $nativeRoot 'unrelated-cwd'
|
|
$nativePath = 'C:\Windows\System32;C:\Windows;' + $nativeBin
|
|
$nativeReceipt = Join-Path $nativeRoot 'launch.json'
|
|
$nativeTerminal = Join-Path $nativeRoot 'normal-close.json'
|
|
$nativeStdout = Join-Path $nativeRoot 'logs/studio.stdout.log'
|
|
$nativeStderr = Join-Path $nativeRoot 'logs/studio.stderr.log'
|
|
$nativeUtf8 = [Text.UTF8Encoding]::new($false)
|
|
|
|
function Read-NativeItem([string]$Path) {
|
|
$item = Get-Item -LiteralPath $Path -Force
|
|
$ancestor = $item
|
|
while ($null -ne $ancestor) {
|
|
if (($ancestor.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw "Reparse path requires separate review: $($ancestor.FullName)"
|
|
}
|
|
$ancestor = if ($ancestor -is [IO.FileInfo]) { $ancestor.Directory } else { $ancestor.Parent }
|
|
}
|
|
return $item
|
|
}
|
|
function Read-NativeFile([string]$Path) {
|
|
$item = Read-NativeItem $Path
|
|
if ($item.PSIsContainer) { throw "Expected file: $Path" }
|
|
return [ordered]@{ path = $item.FullName; bytes = $item.Length; sha256 = (Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash }
|
|
}
|
|
function Write-NativeRecord($Record, [IO.FileStream]$Stream) {
|
|
$bytes = $nativeUtf8.GetBytes(($Record | ConvertTo-Json -Depth 12) + "`n")
|
|
$Stream.Position = 0
|
|
$Stream.SetLength(0)
|
|
$Stream.Write($bytes, 0, $bytes.Length)
|
|
$Stream.Flush($true)
|
|
}
|
|
function Open-NativeOutput([string]$Path) {
|
|
return [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
|
|
}
|
|
|
|
# Source provenance is the closed a57 gate, never current dirty sources or target/debug.
|
|
$nativeManifestFile = Read-NativeFile (Join-Path $nativeRoot 'preparation-manifest.json')
|
|
if ($nativeManifestFile.sha256 -ne $nativeManifestHash) { throw 'Preparation manifest changed.' }
|
|
$nativeManifest = Get-Content -LiteralPath $nativeManifestFile.path -Raw | ConvertFrom-Json
|
|
if ($nativeManifest.source_commit -ne $nativeFreeze -or $nativeManifest.input_manifest_sha256 -ne $nativeInputHash -or $nativeManifest.frozen_input_count -ne 529) { throw 'Unexpected frozen input identity.' }
|
|
foreach ($file in $nativeManifest.files) {
|
|
$path = [IO.Path]::GetFullPath((Join-Path $nativeRoot $file.target))
|
|
if (-not $path.StartsWith($nativeRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Manifest path escapes private root.' }
|
|
$actual = Read-NativeFile $path
|
|
if ($actual.sha256 -ne $file.sha256 -or $actual.bytes -ne $file.bytes) { throw "Prepared input changed: $($file.target)" }
|
|
}
|
|
$nativeInputs = Read-NativeFile (Join-Path $nativeRoot 'provenance/inputs.json')
|
|
if ($nativeInputs.sha256 -ne $nativeInputHash) { throw 'Frozen input manifest changed.' }
|
|
$nativeBuildFile = Read-NativeFile (Join-Path $nativeRoot 'provenance/studio-build-status.json')
|
|
$nativeBuild = Get-Content -LiteralPath $nativeBuildFile.path -Raw | ConvertFrom-Json
|
|
if ($nativeBuild.exit -ne 0 -or $nativeBuild.name -ne 'canvas-union-windows-20260911/studio-build' -or [string]::IsNullOrWhiteSpace($nativeBuild.completed_utc)) { throw 'Matching completed build required.' }
|
|
$nativeGui = Read-NativeFile (Join-Path $nativeBin 'arcadia-studio.exe')
|
|
$nativeDll = Read-NativeFile (Join-Path $nativeBin 'libunwind.dll')
|
|
if ($nativeGui.sha256 -ne $nativeGuiHash -or $nativeGui.bytes -ne 113028608 -or $nativeDll.sha256 -ne $nativeDllHash -or $nativeDll.bytes -ne 190464) { throw 'Immutable GUI/runtime mismatch.' }
|
|
if (-not [string]::Equals($env:SystemRoot, 'C:\Windows', [StringComparison]::OrdinalIgnoreCase)) { throw 'This recorded launch requires SystemRoot C:\Windows.' }
|
|
foreach ($directory in @($nativeProfile, $nativeCwd, (Join-Path $nativeRoot 'logs'), 'C:\Windows\System32')) {
|
|
if (-not (Read-NativeItem $directory).PSIsContainer) { throw "Required directory missing: $directory" }
|
|
}
|
|
foreach ($empty in @($nativeProfile, $nativeCwd)) {
|
|
if (@(Get-ChildItem -LiteralPath $empty -Force).Count -ne 0) { throw "Fresh empty directory required: $empty" }
|
|
}
|
|
foreach ($output in @($nativeReceipt, $nativeTerminal, $nativeStdout, $nativeStderr)) {
|
|
if (Test-Path -LiteralPath $output) { throw "Never overwrite prior launch evidence: $output" }
|
|
}
|
|
|
|
$nativeState = [ordered]@{
|
|
source_commit = $nativeFreeze
|
|
scope = 'Independent pre-grapheme a57 camera/clipping/zoom/hit session; no package or new combined Unicode acceptance implied.'
|
|
preparation_manifest = $nativeManifestFile
|
|
build_inputs_manifest = $nativeInputs
|
|
build_status_file = $nativeBuildFile
|
|
build_status = $nativeBuild
|
|
studio = $nativeGui
|
|
runtime = $nativeDll
|
|
launch_script = (Read-NativeFile $PSCommandPath)
|
|
process_path = $nativePath
|
|
isolated_localappdata = $nativeProfile
|
|
working_directory = $nativeCwd
|
|
arguments = @()
|
|
environment_scope = 'Child ProcessStartInfo only; other normal inherited environment retained.'
|
|
window_style = 'Hidden'
|
|
use_shell_execute = $false
|
|
create_no_window = $true
|
|
stdout = $nativeStdout
|
|
stderr = $nativeStderr
|
|
pid = $null
|
|
launch_requested_utc = $null
|
|
process_started_utc = $null
|
|
state = 'preflight_passed'
|
|
acceptance = 'Pending root observation and independent source/model checks.'
|
|
}
|
|
$nativeReceiptStream = $null
|
|
$nativeOutStream = $null
|
|
$nativeErrStream = $null
|
|
$nativeProcess = $null
|
|
$nativeStarted = $false
|
|
try {
|
|
# Exclusive receipt reservation also serializes accidental concurrent invocations.
|
|
$nativeReceiptStream = Open-NativeOutput $nativeReceipt
|
|
Write-NativeRecord $nativeState $nativeReceiptStream
|
|
$nativeOutStream = Open-NativeOutput $nativeStdout
|
|
$nativeErrStream = Open-NativeOutput $nativeStderr
|
|
$nativeInfo = [Diagnostics.ProcessStartInfo]::new()
|
|
$nativeInfo.FileName = $nativeGui.path
|
|
$nativeInfo.WorkingDirectory = $nativeCwd
|
|
$nativeInfo.UseShellExecute = $false
|
|
$nativeInfo.CreateNoWindow = $true
|
|
$nativeInfo.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
|
|
$nativeInfo.RedirectStandardOutput = $true
|
|
$nativeInfo.RedirectStandardError = $true
|
|
$nativeInfo.Environment['PATH'] = $nativePath
|
|
$nativeInfo.Environment['LOCALAPPDATA'] = $nativeProfile
|
|
foreach ($file in @($nativeGui, $nativeDll, $nativeManifestFile, $nativeInputs, $nativeBuildFile)) {
|
|
if ((Read-NativeFile $file.path).sha256 -ne $file.sha256) { throw "Launch-critical input changed: $($file.path)" }
|
|
}
|
|
$nativeProcess = [Diagnostics.Process]::new()
|
|
$nativeProcess.StartInfo = $nativeInfo
|
|
$nativeState.launch_requested_utc = [DateTime]::UtcNow.ToString('o')
|
|
if (-not $nativeProcess.Start()) { throw 'Process.Start returned false.' }
|
|
$nativeStarted = $true
|
|
$nativeState.pid = $nativeProcess.Id
|
|
# Pump both raw streams concurrently; no line decoding/newline normalization.
|
|
$nativeOutCopy = $nativeProcess.StandardOutput.BaseStream.CopyToAsync($nativeOutStream)
|
|
$nativeErrCopy = $nativeProcess.StandardError.BaseStream.CopyToAsync($nativeErrStream)
|
|
$nativeState.process_started_utc = $nativeProcess.StartTime.ToUniversalTime().ToString('o')
|
|
$nativeState.state = 'launched'
|
|
Write-NativeRecord $nativeState $nativeReceiptStream
|
|
$nativeReceiptStream.Dispose()
|
|
$nativeReceiptStream = $null
|
|
$nativeState | ConvertTo-Json -Depth 12 | Write-Output
|
|
# Deliberately stays attached as a logger. Root uses a running shell session and closes GUI normally.
|
|
$nativeProcess.WaitForExit()
|
|
[Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($nativeOutCopy, $nativeErrCopy))
|
|
$nativeOutStream.Flush($true)
|
|
$nativeErrStream.Flush($true)
|
|
$nativeOutStream.Dispose()
|
|
$nativeOutStream = $null
|
|
$nativeErrStream.Dispose()
|
|
$nativeErrStream = $null
|
|
$nativeClosed = [ordered]@{
|
|
source_commit = $nativeFreeze
|
|
pid = $nativeState.pid
|
|
process_started_utc = $nativeState.process_started_utc
|
|
observed_exit_utc = [DateTime]::UtcNow.ToString('o')
|
|
exit_code = $nativeProcess.ExitCode
|
|
scope = 'Process termination and closed raw logs only; UI acceptance and reason for close require root observation.'
|
|
stdout = (Read-NativeFile $nativeStdout)
|
|
stderr = (Read-NativeFile $nativeStderr)
|
|
studio_readback = (Read-NativeFile $nativeGui.path)
|
|
runtime_readback = (Read-NativeFile $nativeDll.path)
|
|
}
|
|
$nativeCloseStream = Open-NativeOutput $nativeTerminal
|
|
try { Write-NativeRecord $nativeClosed $nativeCloseStream } finally { $nativeCloseStream.Dispose() }
|
|
$nativeClosed | ConvertTo-Json -Depth 12 | Write-Output
|
|
} catch {
|
|
if ($null -ne $nativeReceiptStream) {
|
|
$nativeState.state = if ($nativeStarted) { 'launched_record_error' } else { 'launch_failed' }
|
|
$nativeState['error'] = $_.Exception.Message
|
|
Write-NativeRecord $nativeState $nativeReceiptStream
|
|
}
|
|
throw
|
|
} finally {
|
|
if ($null -ne $nativeReceiptStream) { $nativeReceiptStream.Dispose() }
|
|
if ($null -ne $nativeOutStream) { $nativeOutStream.Dispose() }
|
|
if ($null -ne $nativeErrStream) { $nativeErrStream.Dispose() }
|
|
if ($null -ne $nativeProcess) { $nativeProcess.Dispose() }
|
|
# Never kill an application here. Root owns every native interaction and any recovery.
|
|
}
|