param( [string]$ELF = '../starlet-ios-rust/target/armebv5te-none-eabi/release/revolution-ios-bootstrap', [string]$BuildDirectory = '.starlet_msvc2', [string]$UserDirectory = '.starlet_user3', [ValidateSet('Idle', 'Panic', 'Fatal', 'Fault', 'Phase2', 'Phase3Raw', 'Phase3Ios', 'Phase4Fs', 'Phase5Dir', 'Phase5Es', 'Phase5Client', 'Phase6Write', 'Phase6Verify', 'Phase8Client', 'Phase9Resolve', 'Phase9Launch', 'Phase9Menu', 'Phase9Rescue', 'Phase10AOh1')][string]$ExpectedOutcome = 'Idle', [string]$PpcDol = '', [int]$TimeoutSeconds = 30, # Phase9Launch: the launched program's minimum check count. 5 for the # P09-H02 payload and a requested P09-H02M launch; 3 for a P09-H02M launch # made by the kernel's watchdog (no launcher mailbox to check). [ValidateRange(1, 5)][int]$Phase9MinimumChecks = 5, # Phase10AOh1: the program's minimum check count. 40 for the P10A-H07 # reduced smoke; 16 for the P10A-H07B launched payload. [ValidateRange(1, 1000)][int]$Phase10AMinimumChecks = 40, [switch]$JIT, [switch]$FullDolphin ) $ErrorActionPreference = 'Stop' $repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $elfCandidate = if ([System.IO.Path]::IsPathRooted($ELF)) { $ELF } else { Join-Path $repoRoot $ELF } $elfPath = (Resolve-Path -LiteralPath $elfCandidate).Path $dolPath = if ($PpcDol) { (Resolve-Path -LiteralPath $PpcDol).Path } else { '' } $probePath = Join-Path $repoRoot "$BuildDirectory/Binaries/Tests/StarletBootstrapProbe.exe" if (-not $FullDolphin) { if ($PpcDol) { throw 'PpcDol requires FullDolphin' } if (-not (Test-Path -LiteralPath $probePath -PathType Leaf)) { throw "Build StarletBootstrapProbe first: $probePath" } $probeArguments = @($elfPath) if ($JIT) { $probeArguments += '--jit' } if ($ExpectedOutcome -eq 'Panic') { $probeArguments += '--expect-panic' } if ($ExpectedOutcome -eq 'Fatal') { $probeArguments += '--expect-fatal-data' } if ($ExpectedOutcome -eq 'Fault') { $probeArguments += '--expect-undefined-fault' } if ($ExpectedOutcome -eq 'Phase2') { $probeArguments += '--expect-phase2' } & $probePath @probeArguments exit $LASTEXITCODE } $dolphinPath = Join-Path $repoRoot "$BuildDirectory/Binaries/DolphinNoGUI.exe" $userPath = (Resolve-Path -LiteralPath (Join-Path $repoRoot $UserDirectory)).Path $logPath = Join-Path $userPath 'Logs/dolphin.log' if (-not (Test-Path -LiteralPath $dolphinPath -PathType Leaf)) { throw "Build DolphinNoGUI first: $dolphinPath" } if ($TimeoutSeconds -lt 1) { throw 'TimeoutSeconds must be positive' } New-Item -ItemType Directory -Force -Path (Split-Path -Parent $logPath) | Out-Null $bootArguments = if ($dolPath) { @('-e', "`"$dolPath`"") } else { @('-n', '0000000100000002') } $arguments = @('-u', "`"$userPath`"") + $bootArguments + @( '-C', 'Dolphin.Core.WiiIOSLLE=True', '-C', 'Dolphin.Core.WiiSDCard=False', '-C', "Dolphin.Core.WiiStarletJIT=$($JIT.IsPresent)", '-C', "`"Dolphin.General.WiiStarletBootstrapELF=$elfPath`"", '-C', 'Logger.Options.WriteToFile=True', '-C', 'Logger.Options.Verbosity=4', '-C', 'Logger.Logs.IOS=True', '-v', 'Null', '-p', 'headless' ) $position = if (Test-Path -LiteralPath $logPath) { (Get-Item -LiteralPath $logPath).Length } else { 0 } $pending = '' $trace = [System.Collections.Generic.List[string]]::new() $rescueStarted = $false $outcome = $null $faultLine = $null $probeCounters = $null $probeChecks = $null $dolphin = Start-Process -FilePath $dolphinPath -ArgumentList $arguments ` -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden try { $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { if (Test-Path -LiteralPath $logPath) { $stream = [System.IO.File]::Open($logPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) try { if ($position -gt $stream.Length) { $position = 0; $pending = '' } [void]$stream.Seek($position, [System.IO.SeekOrigin]::Begin) $reader = [System.IO.StreamReader]::new($stream, [System.Text.Encoding]::UTF8, $false, 4096, $true) try { $chunk = $reader.ReadToEnd() } finally { $reader.Dispose() } $position = $stream.Position } finally { $stream.Dispose() } if ($chunk.Length -gt 0) { $lines = ($pending + $chunk) -split "`n" $pending = $lines[-1] for ($index = 0; $index -lt $lines.Length - 1; ++$index) { $line = $lines[$index] if ($line -match 'Starlet bootstrap: (.+)$') { $message = $Matches[1].TrimEnd("`r") $trace.Add($message) Write-Host $message if ($message -eq 'REV:BOOT:IDLE' -and $ExpectedOutcome -notin @('Phase3Raw', 'Phase3Ios', 'Phase4Fs', 'Phase5Dir', 'Phase5Es', 'Phase5Client', 'Phase6Write', 'Phase6Verify', 'Phase8Client', 'Phase9Launch', 'Phase9Menu', 'Phase9Rescue', 'Phase10AOh1')) { $outcome = if ($ExpectedOutcome -eq 'Phase2') { 'Phase2' } else { 'Idle' } } elseif ($message -like 'REV:PANIC*') { $outcome = 'Panic' } # Revolution IOS Phase 09 P09-H01: the read-only System Menu resolution ends. elseif ($ExpectedOutcome -eq 'Phase9Resolve' -and $message -like 'REV:P9:H01 DONE*') { $outcome = 'Phase9Resolve' } elseif ($ExpectedOutcome -eq 'Phase9Resolve' -and $message -like 'REV:P9:H01 *FAIL*') { $outcome = 'Phase9ResolveFailed' } # Revolution IOS Phase 09 P09-H04: the System Menu's observation ends with the kernel's # summary (no request for a while, or none at all); a load failure ends it too. elseif ($ExpectedOutcome -eq 'Phase9Menu' -and ($message -like 'REV:P9:H04 QUIET *' -or $message -like 'REV:P9:H04 NOIPC *')) { $outcome = 'Phase9Menu' } elseif ($ExpectedOutcome -eq 'Phase9Menu' -and $message -like 'REV:P9:H0[34] FAIL*') { $outcome = 'Phase9MenuFailed' } # P09-H04 rescue: the kernel stopped the Menu and launches the rescue screen, whose # status (0x52500005 once drawn) ends the run below. elseif ($ExpectedOutcome -eq 'Phase9Rescue' -and $message -like 'REV:P9:H04 RESCUE GO *') { $rescueStarted = $true } elseif ($ExpectedOutcome -eq 'Phase9Rescue' -and ($message -like 'REV:P9:H0[34] FAIL*' -or $message -like 'REV:P9:H04 RESCUE PLAN FAIL*')) { $outcome = 'Phase9RescueFailed' } elseif ($message -like 'REV:FATAL*') { $outcome = 'Fatal' } } elseif ($line -match 'Starlet bootstrap exception|Starlet bootstrap trace length is invalid') { $outcome = 'Fault' $faultLine = $line.TrimEnd("`r") Write-Host $faultLine } elseif ($line -match 'Broadway ios-probe status=(0x[0-9a-f]+)') { $probeStatus = $Matches[1] Write-Host "PPC: $probeStatus" if ($probeStatus -eq '0x52500002') { $outcome = 'Phase3Raw' } elseif ($probeStatus -eq '0x52500005' -and $ExpectedOutcome -eq 'Phase10AOh1') { $outcome = 'Phase10AOh1' } elseif ($probeStatus -eq '0x52500003') { $outcome = 'Phase3Ios' } # Phase 09 P09-H02: the payload's success is final only with the kernel's # release and first-request lines (checked after each chunk below). elseif ($probeStatus -eq '0x52500005' -and $ExpectedOutcome -eq 'Phase9Launch') { $payloadPassed = $true } elseif ($ExpectedOutcome -eq 'Phase9Rescue') { if ($probeStatus -eq '0x52500005' -and $rescueStarted) { $outcome = 'Phase9Rescue' } } elseif ($probeStatus -eq '0x52500005') { $outcome = $(if ($ExpectedOutcome -in @('Phase5Dir', 'Phase5Es', 'Phase5Client', 'Phase6Write', 'Phase6Verify', 'Phase8Client')) { $ExpectedOutcome } else { 'Phase4Fs' }) } # Phase 09 P09-H02M: a later program's status (the launched monitor # starting) voids an earlier program's pass and its checks. elseif ($ExpectedOutcome -eq 'Phase9Launch' -and $probeStatus -notmatch '^0x5250e') { $payloadPassed = $false; $probeChecks = $null } elseif ($probeStatus -match '^0x5250e') { $outcome = 'ProbeFailed' } } elseif ($line -match 'Broadway ios-probe counters sent=(\d+) received=(\d+) invalid=(\d+) duplicate=(\d+) timeout=(\d+)') { $probeCounters = @([int]$Matches[1], [int]$Matches[2], [int]$Matches[3], [int]$Matches[4], [int]$Matches[5]) Write-Host "PPC counters: $($probeCounters -join '/')" } elseif ($line -match 'Broadway ios-probe checks=(\d+) failures=(\d+)') { $probeChecks = @([int]$Matches[1], [int]$Matches[2]) Write-Host "PPC checks: $($probeChecks -join '/')" } } } } if (-not $outcome -and $ExpectedOutcome -eq 'Phase9Launch' -and $payloadPassed -and $probeChecks -and @($trace | Where-Object { $_ -like 'REV:P9:H02 GO *' }).Count -eq 1 -and @($trace | Where-Object { $_ -like 'REV:P9:H02 FIRST CMD *' }).Count -eq 1) { $outcome = 'Phase9Launch' } if ($outcome) { break } if ($dolphin.HasExited) { break } Start-Sleep -Milliseconds 200 } if ($outcome -ne $ExpectedOutcome) { $exitDetail = if ($dolphin.HasExited) { "Dolphin exited with code $($dolphin.ExitCode). " } else { '' } throw "$exitDetail Bootstrap outcome '$outcome' (expected '$ExpectedOutcome'); see $logPath" } if ($ExpectedOutcome -eq 'Fault') { if ($faultLine -notmatch 'Starlet bootstrap exception 1: LR=0x[0-9a-f]+ SPSR=0x[0-9a-f]+ PC=0x[0-9a-f]+') { throw "Expected an undefined-instruction exception with registers; see $logPath" } } if ($ExpectedOutcome -eq 'Fatal') { if (-not $trace.Contains('REV:BOOT:ENTRY') -or -not $trace.Contains('REV:BOOT:BSS_OK') -or -not $trace.Contains('REV:FATAL:DATA') -or $trace.Contains('REV:BOOT:DATA_OK')) { throw "Fatal data check did not produce the expected markers; see $logPath" } } if ($ExpectedOutcome -eq 'Phase2') { $requiredPhase2 = @('REV:P2:START', 'ARCH PASS', 'CRITICAL PASS', 'TIMER PASS', 'MONOTONIC PASS', 'MEM PASS', 'IRQ PASS', 'BARRIER PASS', 'LOGGING PASS', 'REV:P2:PASS', 'REV:P2:KERNEL_READY', 'REV:BOOT:IDLE') $missingPhase2 = @($requiredPhase2 | Where-Object { -not $trace.Contains($_) }) if ($missingPhase2.Count -gt 0 -or @($trace | Where-Object { $_ -like '* FAIL' }).Count -gt 0) { throw "Phase 02 self-test markers are incomplete; see $logPath" } } if ($ExpectedOutcome -eq 'Phase3Raw' -and (-not $trace.Contains('REV:P3A:START') -or -not $trace.Contains('REV:P3A:ARM_128') -or -not $trace.Contains('REV:P3A:ARM_PASS') -or -not $probeCounters -or $probeCounters[0] -ne 128 -or $probeCounters[1] -ne 128 -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0)) { throw "Phase 03A Starlet markers are incomplete; see $logPath" } if ($ExpectedOutcome -eq 'Phase3Ios' -and (-not $trace.Contains('REV:P3B:START') -or -not $trace.Contains('REV:P3B:OPEN_OK') -or -not $trace.Contains('REV:P3B:PING_OK') -or -not $trace.Contains('REV:P3B:CLOSE_OK') -or -not $probeCounters -or $probeCounters[0] -lt 200 -or $probeCounters[0] -ne $probeCounters[1] -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0 -or -not $probeChecks -or $probeChecks[0] -lt 260 -or $probeChecks[1] -ne 0)) { throw "Phase 03B IOS probe markers or counters are incomplete; see $logPath" } # Revolution IOS Phase 04D: the shared /dev/fs cases (ppc/common/fs_cases.h). if ($ExpectedOutcome -eq 'Phase4Fs' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or -not $probeCounters -or $probeCounters[0] -lt 100 -or $probeCounters[0] -ne $probeCounters[1] -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0 -or -not $probeChecks -or $probeChecks[0] -lt 30 -or $probeChecks[1] -ne 0)) { throw "Phase 04D /dev/fs markers or counters are incomplete; see $logPath" } # Revolution IOS Phase 05B: the READDIR/GETUSAGE cases (ppc/common/fs_dir_cases.h). if ($ExpectedOutcome -eq 'Phase5Dir' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or -not $probeCounters -or $probeCounters[0] -lt 600 -or $probeCounters[0] -ne $probeCounters[1] -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0 -or -not $probeChecks -or $probeChecks[0] -lt 13 -or $probeChecks[1] -ne 0)) { throw "Phase 05B READDIR/GETUSAGE markers or counters are incomplete; see $logPath" } # Revolution IOS Phase 05C/05D: the /dev/es cases (ppc/common/es_cases.h). if ($ExpectedOutcome -eq 'Phase5Es' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or -not $probeCounters -or $probeCounters[0] -lt 600 -or $probeCounters[0] -ne $probeCounters[1] -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0 -or -not $probeChecks -or $probeChecks[0] -lt 18 -or $probeChecks[1] -ne 0)) { throw "Phase 05C /dev/es markers or counters are incomplete; see $logPath" } # P10A-H07 reduced smoke: real service on the emulator USB double; no # local loopback and no physical acceptance. No synthetic IPC counters. if ($ExpectedOutcome -eq 'Phase10AOh1' -and (-not $trace.Contains('REV:P10A:OH1 READY REAL USB') -or @($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or -not $probeChecks -or $probeChecks[0] -lt $Phase10AMinimumChecks -or $probeChecks[1] -ne 0)) { throw 'Phase 10A OH1 reduced smoke markers or checks are incomplete' } # Revolution IOS Phase 05F: the unchanged libogc stock client (ppc/stock-client). if ($ExpectedOutcome -eq 'Phase5Client' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or -not $probeCounters -or $probeCounters[0] -lt 16 -or $probeCounters[0] -ne $probeCounters[1] -or -not $probeChecks -or $probeChecks[0] -lt 16 -or $probeChecks[1] -ne 0)) { throw "Phase 05F stock client markers or counters are incomplete; see $logPath" } # Revolution IOS Phase 06H: the writable scenario (ppc/fs-write-probe, mode Write) on the # emulator-only writable kernel over the disposable NAND (REV_BOOTSTRAP_NAND_DISPOSABLE). if ($ExpectedOutcome -eq 'Phase6Write' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P6:FS MOUNT GEN * WRITABLE' }).Count -ne 1 -or @($trace | Where-Object { $_ -like 'REV:P6:FS GEN * REMOUNT *' }).Count -ne 0 -or -not $probeCounters -or $probeCounters[0] -lt 100 -or $probeCounters[0] -ne $probeCounters[1] -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0 -or -not $probeChecks -or $probeChecks[0] -lt 60 -or $probeChecks[1] -ne 0)) { throw "Phase 06H writable scenario markers or counters are incomplete; see $logPath" } # Revolution IOS Phase 06H: the resulting image after a fresh boot of the read-only kernel # (ppc/fs-write-probe, mode Verify). if ($ExpectedOutcome -eq 'Phase6Verify' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or -not $probeCounters -or $probeCounters[0] -lt 30 -or $probeCounters[0] -ne $probeCounters[1] -or $probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or $probeCounters[4] -ne 0 -or -not $probeChecks -or $probeChecks[0] -lt 25 -or $probeChecks[1] -ne 0)) { throw "Phase 06H verification markers or counters are incomplete; see $logPath" } # Revolution IOS Phase 08 H07R (section 69): the libogc read-after-reboot client # (ppc/p8-client/reboot.c), the same boot.dol as on the console, on the emulator-only # writable kernel over the disposable NAND. It writes only its status word (0x52500005 when # every check of its part passed); the part is proven by the NAND copy, outside this script. if ($ExpectedOutcome -eq 'Phase8Client' -and (-not $trace.Contains('REV:P3B:START') -or @($trace | Where-Object { $_ -like 'REV:P6:FS MOUNT GEN * WRITABLE' }).Count -ne 1)) { throw "Phase 08 client markers are incomplete; see $logPath" } # Revolution IOS Phase 09 P09-H02: Broadway stopped and restarted into the payload while the # kernel stays resident; the payload's own checks passed. if ($ExpectedOutcome -eq 'Phase9Launch' -and (@($trace | Where-Object { $_ -like 'REV:P9:H02 GO *' }).Count -ne 1 -or @($trace | Where-Object { $_ -like 'REV:P9:H02 FIRST CMD *' }).Count -ne 1 -or -not $probeChecks -or $probeChecks[0] -lt $Phase9MinimumChecks -or $probeChecks[1] -ne 0)) { throw "Phase 09 launch markers or checks are incomplete; see $logPath" } # Revolution IOS Phase 09 P09-H04: the Menu was resolved, profiled, loaded and released # (M0-M4) before the observation ended. if ($ExpectedOutcome -in @('Phase9Menu', 'Phase9Rescue')) { $missingMenu = @('REV:P9:H04 M0 ', 'REV:P9:H04 M1 ', 'REV:P9:H04 M2 STOPPED LOWMEM VERIFY OK', 'REV:P9:H04 M3 ', 'REV:P9:H04 M4 ') | Where-Object { $prefix = $_; @($trace | Where-Object { $_.StartsWith($prefix) }).Count -ne 1 } if (@($missingMenu).Count -gt 0) { throw "Phase 09 System Menu milestones are incomplete ($($missingMenu -join ', ')); see $logPath" } } $required = @('REV:BOOT:ENTRY', 'REV:BOOT:BSS_OK', 'REV:BOOT:DATA_OK', 'REV:BOOT:STACK_OK', 'REV:BOOT:RUST', 'Revolution IOS', 'Starlet Rust bootstrap reached') $missing = @($required | Where-Object { -not $trace.Contains($_) }) if ($ExpectedOutcome -in @('Idle', 'Phase2') -and $missing.Count -gt 0) { throw "Bootstrap reached idle without all expected markers; see $logPath" } Write-Host "Starlet bootstrap $outcome passed ($($trace.Count) trace lines)." } finally { if (-not $dolphin.HasExited) { Stop-Process -Id $dolphin.Id $dolphin.WaitForExit() } }