返回 DeepSeek-Reasonix
test-windows-startup-recovery.ps1
根目录 / scripts / test-windows-startup-recovery.ps1
1 [CmdletBinding(DefaultParameterSetName='Portable')]
2 param(
3 [Parameter(Mandatory=$true, ParameterSetName='Portable')][string]$PortableZip,
4 [Parameter(Mandatory=$true, ParameterSetName='Installed')][string]$InstallRoot,
5 [string]$ExpectedVersion = '',
6 [string]$ArtifactPath = '',
7 [string]$DataHome = '',
8 [string]$ExpectedVisibleText = '',
9 [switch]$PrepareHistoricalSession,
10 [string]$EvidenceDirectory = (Join-Path $env:TEMP ('reasonix-recovery-' + [guid]::NewGuid().ToString('N')))
11 )
12 $ErrorActionPreference = 'Stop'
13 . (Join-Path $PSScriptRoot 'windows-acceptance-environment.ps1')
14 . (Join-Path $PSScriptRoot 'windows-upgrade-ui-evidence.ps1')
15 New-Item -ItemType Directory -Force $EvidenceDirectory | Out-Null
16 $install = if ($PSCmdlet.ParameterSetName -eq 'Installed') { [IO.Path]::GetFullPath($InstallRoot) } else { Join-Path $EvidenceDirectory 'install' }
17 $dataHome = if ($DataHome) { [IO.Path]::GetFullPath($DataHome) } else { Join-Path $EvidenceDirectory 'home' }
18 if ($PSCmdlet.ParameterSetName -eq 'Portable') {
19 if (Test-Path $install) { throw 'Evidence install directory must be new; refusing to overwrite a running fixture.' }
20 Expand-Archive -LiteralPath $PortableZip -DestinationPath $install
21 } elseif (-not (Test-Path -LiteralPath $install -PathType Container)) {
22 throw "Installed Reasonix directory is missing: $install"
23 }
24 $current = Get-Content (Join-Path $install 'current.json') -Raw | ConvertFrom-Json
25 if ($ExpectedVersion -and $current.activeVersion -ne $ExpectedVersion) {
26 throw "Installed version mismatch: current.json=$($current.activeVersion), expected=$ExpectedVersion"
27 }
28 $shellPath = Join-Path $install ($current.activeDir + '\app\Reasonix.exe')
29 $launcher = Join-Path $install 'Reasonix.exe'
30 if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) {
31 $launcher = Join-Path $install 'reasonix-launcher.exe'
32 }
33 if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) {
34 throw 'Stable Reasonix launcher is missing from the package root.'
35 }
36
37 function Read-ShellStatus($process) {
38 $pipe = [IO.Pipes.NamedPipeClientStream]::new('.', ('reasonix-shell-v1-' + $process.Id), [IO.Pipes.PipeDirection]::In)
39 try {
40 $pipe.Connect(500)
41 $reader = [IO.StreamReader]::new($pipe)
42 $read = $reader.ReadLineAsync()
43 if (-not $read.Wait(2000)) { return $null }
44 if ($read.Result.Length -gt 16384) { throw 'Oversized status response' }
45 return ($read.Result | ConvertFrom-Json)
46 } catch { return $null } finally { $pipe.Dispose() }
47 }
48
49 function Assert-Ready {
50 foreach ($process in @(Get-Process Reasonix -ErrorAction SilentlyContinue)) {
51 if ($process.Path -ne $shellPath) { continue }
52 $status = Read-ShellStatus $process
53 if ($null -eq $status) { continue }
54 if ($status.schemaVersion -ne 1 -or $status.product -ne 'com.reasonix.desktop' -or $status.pid -ne $process.Id) { throw 'Status identity mismatch' }
55 if ($status.lifecycle -ne 'ready' -or $status.service -ne 'ready' -or -not $status.visible -or -not $status.healthy) { throw ('Not ready: ' + ($status | ConvertTo-Json -Compress)) }
56 if ($status.version -ne $current.activeVersion -or $status.rendererVersion -ne $current.activeVersion) { throw 'Target renderer version mismatch' }
57 $service = Get-Process -Id $status.servicePID
58 $expectedService = Join-Path $install ($current.activeDir + '\reasonix-desktop.exe')
59 if ($service.Path -ne $expectedService) { throw 'Service is outside the active release' }
60 return @{ Shell=$process; Service=$service; Status=$status }
61 }
62 throw 'No verified target shell; inspect preserved logs'
63 }
64
65 function Assert-VisibleContentAndCapture($process, [string]$text, [string]$outputPath) {
66 Add-Type -AssemblyName UIAutomationClient
67 Add-Type -AssemblyName System.Drawing
68 $deadline = [DateTime]::UtcNow.AddSeconds(20)
69 $found = $false
70 $prepared = -not $PrepareHistoricalSession
71 $root = $null
72 while ([DateTime]::UtcNow -lt $deadline -and -not $found) {
73 $process.Refresh()
74 if ($process.MainWindowHandle -ne 0) {
75 $root = [Windows.Automation.AutomationElement]::FromHandle($process.MainWindowHandle)
76 if (-not $prepared) { $prepared = Invoke-PendingHistoricalSession $root }
77 $found = $prepared -and (Test-VisibleUpgradeHistory $root $text)
78 }
79 if (-not $found) { Start-Sleep -Milliseconds 250 }
80 }
81 if ($null -eq $root) { throw 'The packaged shell did not expose a native window for upgrade evidence.' }
82 @(Get-UpgradeUIDescendants $root | Select-Object -First 5000 | ForEach-Object {
83 @{name=$_.Current.Name; id=$_.Current.AutomationId; offscreen=$_.Current.IsOffscreen}
84 }) | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath ($outputPath + '.uia.json') -Encoding utf8
85 $bounds = $root.Current.BoundingRectangle
86 if ($bounds.Width -le 0 -or $bounds.Height -le 0) { throw 'The packaged shell window has invalid bounds.' }
87 $bitmap = [Drawing.Bitmap]::new([int]$bounds.Width, [int]$bounds.Height)
88 try {
89 $graphics = [Drawing.Graphics]::FromImage($bitmap)
90 try { $graphics.CopyFromScreen([int]$bounds.X, [int]$bounds.Y, 0, 0, $bitmap.Size) } finally { $graphics.Dispose() }
91 $bitmap.Save($outputPath, [Drawing.Imaging.ImageFormat]::Png)
92 } finally { $bitmap.Dispose() }
93 if (-not $found) { throw "Packaged UI did not expose expected upgraded history text: $text" }
94 }
95
96 $acceptanceEnvironment = Enter-WindowsAcceptanceEnvironment -DataHome $dataHome
97 try {
98 $attempt = Start-Process $launcher -PassThru -RedirectStandardOutput (Join-Path $EvidenceDirectory 'launcher.stdout.log') -RedirectStandardError (Join-Path $EvidenceDirectory 'launcher.stderr.log')
99 $null = $attempt.Handle
100 if (-not $attempt.WaitForExit(40000)) { throw 'Stable launcher timed out; inspect launcher logs' }
101 if ($attempt.ExitCode -ne 0) { throw ('Stable launcher failed: exit=' + $attempt.ExitCode + '; ' + (Get-Content (Join-Path $EvidenceDirectory 'launcher.stderr.log') -Raw)) }
102 $first = Assert-Ready
103 $again = Start-Process $launcher -PassThru
104 $null = $again.Handle
105 if (-not $again.WaitForExit(40000) -or $again.ExitCode -ne 0) { throw 'Second launch failed or timed out' }
106 $second = Assert-Ready
107 if ($first.Shell.Id -ne $second.Shell.Id -or $first.Service.Id -ne $second.Service.Id -or $first.Status.generation -ne $second.Status.generation) { throw 'Second launch replaced the healthy instance' }
108 if ($ExpectedVisibleText) {
109 Assert-VisibleContentAndCapture $first.Shell $ExpectedVisibleText (Join-Path $EvidenceDirectory 'upgraded-history.png')
110 }
111 Start-Process $shellPath -ArgumentList '--reasonix-lifecycle-request=quit' | Out-Null
112 if (-not $first.Shell.WaitForExit(15000) -or -not $first.Service.WaitForExit(1000)) { throw 'Normal exit left a shell or service alive' }
113 $shellLog = Get-Content (Join-Path $dataHome 'desktop-shell\logs\shell.log') -Raw
114 if ($shellLog -match 'exit deadline exceeded|killing it|termination failed') { throw 'Forced cleanup is not a normal-exit pass' }
115 $artifactHash = if ($ArtifactPath) { (Get-FileHash -Algorithm SHA256 $ArtifactPath).Hash } elseif ($PortableZip) { (Get-FileHash -Algorithm SHA256 $PortableZip).Hash } else { '' }
116 $report = @{
117 artifact=$artifactHash
118 version=$current.activeVersion
119 OS=[Environment]::OSVersion.VersionString
120 architecture=$env:PROCESSOR_ARCHITECTURE
121 shellPID=$first.Shell.Id
122 servicePID=$first.Service.Id
123 startup='passed'; secondLaunch='passed'; normalExit='passed'; visibleHistory=if ($ExpectedVisibleText) { 'passed' } else { 'not requested' }
124 signing='not checked'; installer=if ($PSCmdlet.ParameterSetName -eq 'Installed') { 'passed' } else { 'not exercised' }; recoveryConsent='not exercised'
125 }
126 $report | ConvertTo-Json | Set-Content (Join-Path $EvidenceDirectory 'result.json')
127 $report | ConvertTo-Json
128 } finally {
129 Restore-WindowsAcceptanceEnvironment -Snapshot $acceptanceEnvironment
130 Write-Host "Evidence preserved at $EvidenceDirectory. No forced process cleanup was performed."
131 }
132
132 lines Plain Text