XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
param(
    [Parameter(Mandatory = $true)][string]$ImagePath,
    [Parameter(Mandatory = $true)][string]$VerifiedBootResult,
    [string]$VerifiedTcgBootResult,
    [string]$QemuPath,
    [switch]$Fullscreen,
    [switch]$FitToWindow,
    [switch]$NativePixels,
    [switch]$PlanOnly,
    [ValidateSet('auto','tcg','whpx')][string]$Acceleration = 'auto',
    [ValidateSet('gtk','sdl')][string]$DisplayBackend = 'gtk'
)
$ErrorActionPreference = 'Stop'
if ($FitToWindow -and $NativePixels) { throw 'FitToWindow and NativePixels are mutually exclusive viewport policies.' }
if ($DisplayBackend -eq 'sdl' -and $NativePixels) { throw 'SDL uses automatic window scaling; NativePixels requires the GTK display backend.' }
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$sourceImage = (Resolve-Path -LiteralPath $ImagePath).Path
$receiptPath = (Resolve-Path -LiteralPath $VerifiedBootResult).Path
$receipt = Get-Content -LiteralPath $receiptPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ($receipt.Passed -ne $true -or $receipt.ImageUnchanged -ne $true -or $receipt.QemuExitCode -ne 0) {
    throw 'The source image must have a successful, cleaned-up QEMU boot receipt.'
}
$imageHash = (Get-FileHash -LiteralPath $sourceImage -Algorithm SHA256).Hash.ToLowerInvariant()
if ($imageHash -ne $receipt.ImageSha256) { throw 'The image does not match its verified boot receipt.' }
$memoryMiB = if ($receipt.MemoryMiB) { [int]$receipt.MemoryMiB } else { 512 }
if ($memoryMiB -notin @(512,1024,2048)) { throw 'The verified memory profile is unsupported.' }
$verifiedAcceleration = if ($receipt.Acceleration) { [string]$receipt.Acceleration } else { 'tcg' }
if ($verifiedAcceleration -notin @('tcg','whpx')) { throw 'The verified receipt has an unsupported acceleration profile.' }
$tcgReceiptPath = $null
$tcgReceiptHash = $null
if ($VerifiedTcgBootResult) { $tcgReceiptPath = (Resolve-Path -LiteralPath $VerifiedTcgBootResult).Path }
elseif ($verifiedAcceleration -eq 'tcg') { $tcgReceiptPath = $receiptPath }
else {
    $packagedComparison = Join-Path $taskRoot 'Validation/Receipts/Display.json'
    $packagedManifest = Join-Path $taskRoot 'Validation/ReceiptFiles.json'
    if ((Test-Path -LiteralPath $packagedComparison) -and (Test-Path -LiteralPath $packagedManifest)) {
        $manifest = Get-Content -Raw -Encoding UTF8 -LiteralPath $packagedManifest | ConvertFrom-Json
        $comparisonEntry = @($manifest | Where-Object { $_.Name -eq 'Display' -and $_.PackagedPath -eq 'Validation/Receipts/Display.json' })
        if ($comparisonEntry.Count -ne 1 -or (Get-FileHash -LiteralPath $packagedComparison -Algorithm SHA256).Hash.ToLowerInvariant() -ne $comparisonEntry[0].Sha256) { throw 'The packaged TCG comparison receipt failed manifest verification.' }
        $tcgReceiptPath = $packagedComparison
    }
}
if ($tcgReceiptPath) {
    $tcgReceipt = Get-Content -Raw -Encoding UTF8 -LiteralPath $tcgReceiptPath | ConvertFrom-Json
    if ($tcgReceipt.Acceleration -ne 'tcg' -or $tcgReceipt.Passed -ne $true -or $tcgReceipt.ImageUnchanged -ne $true -or $tcgReceipt.QemuExitCode -ne 0 -or
        $tcgReceipt.ImageSha256 -ne $imageHash -or $tcgReceipt.KernelSha256 -notmatch '^[0-9a-f]{64}$' -or $tcgReceipt.SourceSnapshotSha256 -notmatch '^[0-9a-f]{64}$' -or
        $tcgReceipt.KernelSha256 -ne $receipt.KernelSha256 -or $tcgReceipt.SourceSnapshotSha256 -ne $receipt.SourceSnapshotSha256 -or
        ($tcgReceipt.MemoryMiB -and [int]$tcgReceipt.MemoryMiB -ne $memoryMiB)) {
        throw 'The TCG comparison must be a passing, cleaned-up test of the identical image, kernel and source snapshot.'
    }
    $tcgReceiptHash = (Get-FileHash -LiteralPath $tcgReceiptPath -Algorithm SHA256).Hash.ToLowerInvariant()
}
if ($Acceleration -in @('auto','tcg') -and !$tcgReceiptPath) { throw 'Interactive auto/TCG requires a matching TCG boot receipt. Pass -VerifiedTcgBootResult; explicitly select -Acceleration whpx only to opt into the unverified GTK startup path.' }
$selectedAcceleration = if ($Acceleration -eq 'auto') { 'tcg' } else { $Acceleration }
$accelerationReason = if ($Acceleration -eq 'auto') { 'Prefer identical-image verified TCG for interactive display: this host previously timed out in UEFI with WHPX+GTK although headless WHPX acceptance passed.' } else { 'Explicit user-selected accelerator; a headless proof does not guarantee interactive display startup.' }
if (!$QemuPath) {
    $localQemu = Join-Path $taskRoot '.tools/qemu/qemu-system-x86_64.exe'
    if (Test-Path -LiteralPath $localQemu) { $QemuPath = $localQemu }
    else {
        $command = Get-Command qemu-system-x86_64.exe -ErrorAction SilentlyContinue
        if ($null -eq $command) { throw 'Pass -QemuPath with the installed qemu-system-x86_64.exe path.' }
        $QemuPath = $command.Source
    }
}
$QemuPath = (Resolve-Path -LiteralPath $QemuPath).Path
$displayProbe = Join-Path $taskRoot 'Tools/CSharpBoot/ReadHostDisplay.ps1'
if (!(Test-Path -LiteralPath $displayProbe)) { throw 'The read-only host display probe is missing.' }
$hostDisplays = @(& $displayProbe)
$primaryDisplay = $hostDisplays | Where-Object { $_.Primary } | Select-Object -First 1
if ($null -eq $primaryDisplay) { $primaryDisplay = $hostDisplays | Select-Object -First 1 }
if ($null -eq $primaryDisplay) { throw 'No active display mode was returned by Windows; cannot choose the native guest display size.' }
$nativeWidth = [int]$primaryDisplay.MaximumWidth
$nativeHeight = [int]$primaryDisplay.MaximumHeight
$targetRefreshHz = [int]$primaryDisplay.MaximumRefreshHzAtMaximumResolution
if ([long]$nativeWidth * $nativeHeight * 4 -gt 64MB) { throw 'The detected native display mode exceeds the 64 MiB virtual VGA framebuffer budget.' }
$savedErrorAction = $ErrorActionPreference
try {
    $ErrorActionPreference = 'Continue'
    $vgaHelp = (& $QemuPath '-device' 'VGA,help' 2>&1 | Out-String)
    $probeExitCode = $LASTEXITCODE
} finally { $ErrorActionPreference = $savedErrorAction }
if ($probeExitCode -ne 0 -or $vgaHelp -notmatch '(?m)^\s*xres=' -or $vgaHelp -notmatch '(?m)^\s*yres=' -or $vgaHelp -notmatch '(?m)^\s*xmax=' -or $vgaHelp -notmatch '(?m)^\s*ymax=' -or $vgaHelp -notmatch '(?m)^\s*edid=') {
    throw 'This QEMU VGA device does not expose the required native-resolution EDID properties.'
}
$hasRefreshProperty = $vgaHelp -match '(?m)^\s*refresh_rate='
# Fit the complete framebuffer into an ordinary maximized window by default.
# This is host presentation only: native guest pixels and guest UI scale stay independent.
$fitViewport = !$NativePixels
$zoomMode = if ($fitViewport) { 'on' } else { 'off' }
$displayArgument = if ($DisplayBackend -eq 'sdl') { 'sdl,gl=off' } else { 'gtk,zoom-to-fit=' + $zoomMode }
$requestedWindowStyle = if ($fitViewport -and !$Fullscreen) { 'Maximized' } else { 'Normal' }
$firmwareDirectory = Join-Path $taskRoot '.tools/firmware'
if (!(Test-Path -LiteralPath $firmwareDirectory)) { $firmwareDirectory = Join-Path $taskRoot 'Firmware' }
$firmwareCode = Join-Path $firmwareDirectory 'edk2-x86_64-code.fd'
$firmwareVars = Join-Path $firmwareDirectory 'edk2-i386-vars.fd'
if (!(Test-Path -LiteralPath $firmwareCode) -or !(Test-Path -LiteralPath $firmwareVars)) { throw 'The pinned EDK2 firmware files are missing.' }
$dependencyPath = Join-Path $taskRoot 'Tools/CSharpBoot/Dependencies.json'
if (!(Test-Path -LiteralPath $dependencyPath)) { $dependencyPath = Join-Path $taskRoot 'Validation/Dependencies.json' }
$dependencies = Get-Content -LiteralPath $dependencyPath -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($firmware in @($firmwareCode, $firmwareVars)) {
    $name = [IO.Path]::GetFileName($firmware)
    if ((Get-FileHash -LiteralPath $firmware -Algorithm SHA256).Hash.ToLowerInvariant() -ne $dependencies.Firmware.$name) { throw ('Pinned firmware verification failed: ' + $name) }
}
$sessionDirectory = Join-Path $taskRoot ('build/CSharp/Interactive-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
[void][IO.Directory]::CreateDirectory($sessionDirectory)
$sessionImage = Join-Path $sessionDirectory 'System.img'
$sessionVars = Join-Path $sessionDirectory 'FirmwareVars.fd'
$serialLog = Join-Path $sessionDirectory 'Serial.log'
Copy-Item -LiteralPath $sourceImage -Destination $sessionImage
Copy-Item -LiteralPath $firmwareVars -Destination $sessionVars
if ((Get-FileHash -LiteralPath $sessionImage -Algorithm SHA256).Hash.ToLowerInvariant() -ne $imageHash) { throw 'Interactive image copy failed verification.' }
$arguments = @('-name', 'XFESystem | XFEstudio - CSharp', '-machine', 'q35', '-accel', $selectedAcceleration, '-cpu', 'max', '-smp', '1', '-m', ($memoryMiB.ToString() + 'M'),
    '-vga', 'std', '-global', 'VGA.vgamem_mb=64',
    '-global', ('VGA.xres=' + $nativeWidth), '-global', ('VGA.yres=' + $nativeHeight),
    '-global', ('VGA.xmax=' + $nativeWidth), '-global', ('VGA.ymax=' + $nativeHeight), '-global', 'VGA.edid=on',
    '-drive', ('if=pflash,format=raw,readonly=on,file=' + $firmwareCode.Replace(',', ',,')),
    '-drive', ('if=pflash,format=raw,file=' + $sessionVars.Replace(',', ',,')),
    '-drive', ('file=' + $sessionImage.Replace(',', ',,') + ',format=raw,if=ide,snapshot=on'),
    '-display', $displayArgument, '-serial', ('file:' + $serialLog), '-monitor', 'none', '-no-reboot')
# QEMU's EDID refresh_rate is in millihertz, including for its DisplayID extension.
if ($hasRefreshProperty) { $arguments += @('-global', ('VGA.refresh_rate=' + ($targetRefreshHz * 1000))) }
if ($receipt.NativeHttpPassed -eq $true) { $arguments += @('-netdev', 'user,id=Network', '-device', 'e1000e,netdev=Network') }
else { $arguments += @('-net', 'none') }
if ($Fullscreen) { $arguments += '-full-screen' }
function Quote-NativeArgument([string]$value) {
    if ($value.Contains('"')) { throw 'Unexpected quotation mark in a native argument.' }
    return '"' + ($value -replace '(\\+)$', '$1$1') + '"'
}
$argumentString = ($arguments | ForEach-Object { Quote-NativeArgument $_ }) -join ' '
$process = $null
$launch = $null
try {
$viewportPolicy = if ($fitViewport) { 'FitCompleteDesktop' } else { 'NativePixelsMayClip' }
$displayMetadata = [ordered]@{HostDisplays=$hostDisplays;SelectedHostDisplay=$primaryDisplay.DeviceName;NativeWidth=$nativeWidth;NativeHeight=$nativeHeight;TargetRefreshHz=$targetRefreshHz;QemuRefreshRateMilliHz=($targetRefreshHz * 1000);QemuRefreshPropertyAvailable=$hasRefreshProperty;QemuEdidRefreshHintApplied=$hasRefreshProperty;RefreshMeaning='EDID and renderer target only; actual GPU VSync and 240 presented frames per second are not guaranteed';ZoomToFit=[bool]$fitViewport;ViewportPolicy=$viewportPolicy;HostViewportResampling=[bool]$fitViewport;NativePixelSamplingRequested=[bool]$NativePixels;GuestFramebufferResolutionUnchanged=$true;RequestedWindowStyle=$requestedWindowStyle;HostDpiManagedBy='GTK and Windows; the launcher does not alter system DPI settings';FullscreenExplicitOptIn=[bool]$Fullscreen;DisplayBackend=$DisplayBackend;DisplayBackendChoice=$DisplayBackend;QemuDisplayArgument=$displayArgument;HostGlEnabled=$false;SdlGlEnabled=$false;Acceleration=$selectedAcceleration;VerifiedAcceleration=$verifiedAcceleration;AccelerationChoice=$Acceleration;InteractiveAccelerationPolicy='PreferIdenticalImageVerifiedTCGForGTK';AccelerationReason=$accelerationReason;VerifiedTcgBootResult=$tcgReceiptPath;VerifiedTcgReceiptSha256=$tcgReceiptHash;VerifiedTcgComparisonImageMatch=[bool]$tcgReceiptPath;HeadlessProofIsNotGtkStartupProof=$true;WindowsFeaturesChanged=$false}
if ($PlanOnly) {
    $plan = [ordered]@{Kind='CSharpInteractiveQemuPlan';SourceImage=$sourceImage;ImageSha256=$imageHash;VerifiedBootResult=$receiptPath;QemuPath=$QemuPath;Arguments=$arguments;SessionDirectory=$sessionDirectory;Display=$displayMetadata;AutomaticInput=$false;PhysicalDiskAccess=$false;ProcessStarted=$false}
    $planPath = Join-Path $sessionDirectory 'LaunchPlan.json'
    $plan | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $planPath -Encoding UTF8
    Write-Output $planPath
    return
}
$process = Start-Process -FilePath $QemuPath -ArgumentList $argumentString -WorkingDirectory $taskRoot -WindowStyle $requestedWindowStyle -PassThru -RedirectStandardOutput (Join-Path $sessionDirectory 'QemuStdout.log') -RedirectStandardError (Join-Path $sessionDirectory 'QemuStderr.log')
$launch = [ordered]@{Kind='CSharpInteractiveQemu';ProcessId=$process.Id;SourceImage=$sourceImage;ImageSha256=$imageHash;VerifiedBootResult=$receiptPath;QemuPath=$QemuPath;Arguments=$arguments;SessionDirectory=$sessionDirectory;Display=$displayMetadata;Ready=$false;AutomaticInput=$false;PhysicalDiskAccess=$false}
$launchPath = Join-Path $sessionDirectory 'Launch.json'
$launch | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $launchPath -Encoding UTF8
$deadline = [DateTime]::UtcNow.AddSeconds(90)
while ([DateTime]::UtcNow -lt $deadline) {
    if ($process.HasExited) { throw ('QEMU exited before the desktop was ready. See ' + $sessionDirectory) }
    if (Test-Path -LiteralPath $serialLog) {
        $log = Get-Content -LiteralPath $serialLog -Raw -Encoding UTF8
        if ($log -match '\[FAILFAST\]|\*\*\* UNHANDLED EXCEPTION') {
            throw ('The interactive guest failed. See ' + $serialLog)
        }
        if ($log -and $log.Contains('XFE_CSHARP_DESKTOP_READY')) {
            $launch.Ready = $true
            $launch | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $launchPath -Encoding UTF8
            Write-Host ('XFESystem desktop ready. Session: ' + $launchPath)
            return
        }
    }
    Start-Sleep -Milliseconds 200
}
throw ('Desktop startup timed out. See ' + $serialLog)
} catch {
    if ($null -ne $process -and !$process.HasExited -and ($null -eq $launch -or !$launch.Ready)) { $process.Kill(); $process.WaitForExit() }
    throw
}