diff --git a/scripts/Test-FoundryOsRecoveryWinRe.ps1 b/scripts/Test-FoundryOsRecoveryWinRe.ps1 new file mode 100644 index 00000000..9c43cca1 --- /dev/null +++ b/scripts/Test-FoundryOsRecoveryWinRe.ps1 @@ -0,0 +1,341 @@ +<# +.SYNOPSIS +Validate a mounted or extracted WinRE image for Foundry OS Recovery readiness. + +.DESCRIPTION +This script inspects a WinRE working tree for required recovery artifacts and +validates non-recoverable exclusions. Validation is non-destructive by default. +Use `-BootToRecovery` with `-Force` to request a one-time reboot path. + +.PARAMETER WinReRoot +Path to the mounted/extracted WinRE image root. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$WinReRoot, + + [Parameter()] + [string[]]$LauncherCandidates = @( + 'Sources\Recovery\Tools\FoundryRecoveryLauncher.cmd', + 'FoundryRecoveryLauncher.cmd' + ), + + [Parameter()] + [string]$WinReConfigFile = 'WinREConfig.xml', + + [Parameter()] + [string]$CurlExecutablePath = 'Windows\System32\curl.exe', + + [Parameter()] + [string]$SevenZipExecutablePattern = 'Foundry\\Tools\\7zip\\[^\\]+\\7za\.exe$', + + [Parameter()] + [string[]]$ExcludedConfigPatterns = @( + 'Foundry\\Config\\foundry.connect.provisioning-source.txt', + 'Foundry\\Config\\foundry.deploy.provisioning-source.txt', + 'Foundry\\Config\\Secrets\\*', + 'Foundry\\Config\\Network\\*', + 'Foundry\\Config\\Autopilot\\*', + 'Foundry\\Runtime\\AutopilotHash\\*', + 'Foundry\\Tools\\OA3\\*', + 'Foundry\\Config\\*enterprise*.*', + 'Foundry\\Config\\*personalization*.*' + ), + + [Parameter()] + [switch]$BootToRecovery, + + [Parameter()] + [switch]$SkipReAgentC, + + [Parameter()] + [switch]$Force +) + +Set-StrictMode -Version Latest + +function Resolve-AbsolutePath { + param( + [Parameter(Mandatory = $true)] [string]$Path + ) + + $resolved = Resolve-Path -Path $Path -ErrorAction SilentlyContinue + if ($null -eq $resolved) { + return $Path + } + + return $resolved.ProviderPath +} + +function Test-ExistsAny { + param( + [Parameter(Mandatory = $true)] [string]$Root, + [Parameter(Mandatory = $true)] [string[]]$Candidates, + [Parameter()] [switch]$Recurse + ) + + foreach ($candidate in $Candidates) { + $path = Join-Path -Path $Root -ChildPath $candidate + if (Test-Path -LiteralPath $path) { + return $path + } + } + + if (-not $Recurse) { + return $null + } + + $allFiles = Get-ChildItem -Path $Root -Recurse -File -ErrorAction SilentlyContinue + foreach ($candidate in $Candidates) { + $found = $allFiles | Where-Object { $_.Name -like $candidate -or $_.FullName -like "*$candidate" } | Select-Object -First 1 + if ($found) { + return $found.FullName + } + } + + return $null +} + +function Test-MissingForbiddenConfigs { + param( + [Parameter(Mandatory = $true)] [string]$Root, + [Parameter(Mandatory = $true)] [string[]]$Patterns + ) + + $files = Get-ChildItem -Path $Root -Recurse -File -ErrorAction SilentlyContinue + $forbidden = @() + + foreach ($pattern in $Patterns) { + $normalized = Join-Path -Path $Root -ChildPath $pattern + $forbidden += $files | Where-Object { $_.FullName -like $normalized } + } + + return $forbidden | Select-Object -ExpandProperty FullName -Unique +} + +function Test-JsonFile { + param( + [Parameter(Mandatory = $true)] [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return $false + } + + $null = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + return $true +} + +function Assert-Administrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw 'Booting to recovery requires an elevated PowerShell session.' + } +} + +function Test-ReAgentCInfo { + $reagentc = Join-Path -Path $env:SystemRoot -ChildPath 'System32\reagentc.exe' + if (-not (Test-Path -LiteralPath $reagentc)) { + throw "reagentc.exe was not found: $reagentc" + } + + $output = & $reagentc /info 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "reagentc /info failed with exit code $LASTEXITCODE. $($output -join ' ')" + } + + $text = $output -join "`n" + if ($text -notmatch '(?im)Windows RE status:\s*Enabled') { + throw 'Windows RE is not enabled according to reagentc /info.' + } + + if ($text -notmatch '(?im)Windows RE location:\s*.+') { + throw 'Windows RE location is missing from reagentc /info.' + } + + return $text +} + +$checkResults = [System.Collections.Generic.List[string]]::new() +$errors = [System.Collections.Generic.List[string]]::new() +$root = Resolve-AbsolutePath -Path $WinReRoot + +if (-not (Test-Path -LiteralPath $root)) { + throw "WinRE root path does not exist: $root" +} + +$successCount = 0 + +if ($SkipReAgentC) { + $checkResults.Add('SKIP reagentc /info validation') +} else { + try { + $null = Test-ReAgentCInfo + $checkResults.Add('PASS reagentc /info: Windows RE is enabled') + $successCount++ + } catch { + $errors.Add("FAIL reagentc /info validation: $($_.Exception.Message)") + } +} + +# Launcher artifact +$launcher = Test-ExistsAny -Root $root -Candidates $LauncherCandidates +if ($launcher) { + $checkResults.Add("PASS Launcher: $launcher") + $successCount++ + + $launcherContent = Get-Content -LiteralPath $launcher -Raw + if ($launcherContent -match '(?i)powershell|FoundryBootstrap\.ps1') { + $errors.Add('FAIL Launcher still references PowerShell or FoundryBootstrap.ps1') + } else { + $checkResults.Add('PASS Launcher is CMD-only') + $successCount++ + } +} else { + $errors.Add('FAIL Missing WinRE recovery launcher') +} + +# WinREConfig.xml +$winReConfigPath = Test-ExistsAny -Root $root -Candidates @($WinReConfigFile) -Recurse +if ($winReConfigPath) { + $checkResults.Add("PASS WinREConfig: $winReConfigPath") + $successCount++ +} else { + $errors.Add('FAIL Missing WinREConfig.xml') +} + +# Obsolete recovery bootstrap artifacts +$obsoleteBootstrapPath = Test-ExistsAny -Root $root -Candidates @( + 'FoundryBootstrap.ps1', + 'Windows\System32\FoundryBootstrap.ps1', + 'Windows\System32\WinPe\FoundryBootstrap.ps1' +) -Recurse +if ($obsoleteBootstrapPath) { + $errors.Add("FAIL Obsolete FoundryBootstrap.ps1 artifact present: $obsoleteBootstrapPath") +} else { + $checkResults.Add('PASS Obsolete FoundryBootstrap.ps1 absent') + $successCount++ +} + +# curl.exe +$curlPath = Join-Path -Path $root -ChildPath $CurlExecutablePath +if (Test-Path -LiteralPath $curlPath) { + $checkResults.Add("PASS curl.exe: $curlPath") + $successCount++ +} else { + $errors.Add("FAIL Missing curl.exe: $curlPath") +} + +# Foundry.Connect executable +$connectMatches = Get-ChildItem -Path $root -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match 'Foundry\\Runtime\\Foundry\.Connect\\[^\\]+\\Foundry\.Connect\.exe$' } +if ($connectMatches) { + $checkResults.Add("PASS Foundry.Connect: $($connectMatches[0].FullName)") + $successCount++ +} else { + $errors.Add('FAIL Missing Foundry.Connect executable') +} + +# 7-Zip runtime +$sevenZipMatches = Get-ChildItem -Path $root -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match $SevenZipExecutablePattern } +$sevenZipLicense = Join-Path -Path $root -ChildPath 'Foundry\Tools\7zip\License.txt' +$sevenZipReadme = Join-Path -Path $root -ChildPath 'Foundry\Tools\7zip\readme.txt' +if ($sevenZipMatches -and (Test-Path -LiteralPath $sevenZipLicense) -and (Test-Path -LiteralPath $sevenZipReadme)) { + $checkResults.Add("PASS 7-Zip runtime: $($sevenZipMatches[0].FullName)") + $successCount++ +} else { + $errors.Add('FAIL Missing bundled 7-Zip runtime or license files') +} + +# Minimal recovery config files +$connectConfigPath = Join-Path -Path $root -ChildPath 'Foundry\Config\foundry.connect.config.json' +$deployConfigPath = Join-Path -Path $root -ChildPath 'Foundry\Config\foundry.deploy.config.json' +$timeZoneMapPath = Join-Path -Path $root -ChildPath 'Foundry\Config\iana-windows-timezones.json' + +try { + if (Test-JsonFile -Path $connectConfigPath) { + $checkResults.Add("PASS Foundry.Connect config: $connectConfigPath") + $successCount++ + } else { + $errors.Add('FAIL Missing or invalid Foundry.Connect recovery config') + } + + if (Test-JsonFile -Path $deployConfigPath) { + $checkResults.Add("PASS Foundry.Deploy config: $deployConfigPath") + $successCount++ + } else { + $errors.Add('FAIL Missing or invalid sanitized Foundry.Deploy recovery config') + } + + if (Test-JsonFile -Path $timeZoneMapPath) { + $checkResults.Add("PASS IANA time zone map: $timeZoneMapPath") + $successCount++ + } else { + $errors.Add('FAIL Missing or invalid IANA time zone map') + } +} catch { + $errors.Add("FAIL Invalid recovery JSON payload: $($_.Exception.Message)") +} + +# Ensure Foundry.Deploy is absent +$deployArtifacts = Get-ChildItem -Path (Join-Path $root 'Foundry') -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match 'Foundry\\Runtime\\Foundry\.Deploy\\' -or $_.FullName -match 'Foundry\\Deploy\\' -or $_.Name -match '(?i)foundry\.deploy\.exe$' } +if ($deployArtifacts.Count -eq 0) { + $checkResults.Add('PASS Foundry.Deploy absent') + $successCount++ +} else { + $errors.Add(("FAIL Foundry.Deploy present: {0}" -f ($deployArtifacts | Select-Object -First 3 | ForEach-Object { $_.FullName } | Join-String -Separator '; '))) +} + +# Excluded configs must be absent +$forbidden = Test-MissingForbiddenConfigs -Root $root -Patterns $ExcludedConfigPatterns +if ($forbidden.Count -eq 0) { + $checkResults.Add('PASS No excluded recovery-time config files') + $successCount++ +} else { + $errors.Add(("FAIL Forbidden config file(s) found: {0}" -f (($forbidden[0..([Math]::Min($forbidden.Count - 1, 4))]) -join '; '))) +} + +Write-Host 'Foundry OS Recovery WinRE validation' -ForegroundColor Cyan +Write-Host "Root: $root" -ForegroundColor DarkGray +Write-Host '' +foreach ($r in $checkResults) { + Write-Host $r -ForegroundColor Green +} + +if ($errors.Count -gt 0) { + Write-Host '' + Write-Host 'Validation errors:' -ForegroundColor Red + foreach ($e in $errors) { + Write-Host "- $e" -ForegroundColor Red + } + Write-Host "`nResult: FAILED (errors: $($errors.Count), passed: $successCount)" -ForegroundColor Red + + if ($BootToRecovery) { + throw 'Validation failed; boot to recovery skipped by design.' + } + + exit 1 +} + +Write-Host "`nResult: PASSED (checks: $successCount)" -ForegroundColor Green + +if (-not $BootToRecovery) { + Write-Host 'Non-destructive mode complete. Use -BootToRecovery -Force to boot into WinRE.' + return +} + +if (-not $Force) { + throw 'Boot to recovery is explicit and destructive; pass -Force to continue.' +} + +Assert-Administrator + +if ($PSCmdlet.ShouldProcess('local machine', 'boot into Windows Recovery (OS Recovery)')) { + Write-Host 'Booting into Windows Recovery Environment...' + shutdown.exe /r /o /t 0 +} diff --git a/src/Foundry.Connect/Strings/fr-CA/Resources.resx b/src/Foundry.Connect/Strings/fr-CA/Resources.resx index 9f931b49..f6016e78 100644 --- a/src/Foundry.Connect/Strings/fr-CA/Resources.resx +++ b/src/Foundry.Connect/Strings/fr-CA/Resources.resx @@ -87,7 +87,7 @@ Version: {0} - Configuration: {0} + Configuration : {0} Configuration: paramètres par défaut intégrés diff --git a/src/Foundry.Connect/Strings/lv-LV/Resources.resx b/src/Foundry.Connect/Strings/lv-LV/Resources.resx index f69b102d..8b386d7f 100644 --- a/src/Foundry.Connect/Strings/lv-LV/Resources.resx +++ b/src/Foundry.Connect/Strings/lv-LV/Resources.resx @@ -375,7 +375,7 @@ latviešu (Latvija) - Norwegian Bokmal (Norway) + norvēģu bukmols (Norvēģija) holandiešu (Nīderlande) diff --git a/src/Foundry.Connect/Strings/nl-NL/Resources.resx b/src/Foundry.Connect/Strings/nl-NL/Resources.resx index ac938697..8eefd1ca 100644 --- a/src/Foundry.Connect/Strings/nl-NL/Resources.resx +++ b/src/Foundry.Connect/Strings/nl-NL/Resources.resx @@ -303,7 +303,7 @@ Onderneming - {0} (machine) + {0} (computer) {0} (machine of gebruiker) diff --git a/src/Foundry.Connect/Strings/sv-SE/Resources.resx b/src/Foundry.Connect/Strings/sv-SE/Resources.resx index 0b530356..13d45892 100644 --- a/src/Foundry.Connect/Strings/sv-SE/Resources.resx +++ b/src/Foundry.Connect/Strings/sv-SE/Resources.resx @@ -168,7 +168,7 @@ Visa eller dölj Wi-Fi lösenord - Provisioned Wi-Fi + Provisionerat Wi-Fi Profil diff --git a/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs b/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs index dedb7ef3..50f45ce1 100644 --- a/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs +++ b/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs @@ -64,6 +64,32 @@ public void Generate_WhenNetworkProfileRoamingIsEnabled_PropagatesDeployRoaming( Assert.True(result.Network.ProfileRoaming.IncludePrivateKeyMaterial); } + [Fact] + public void Generate_WhenOsRecoveryIsEnabled_PropagatesDeployOsRecoveryFlag() + { + var generator = new DeployConfigurationGenerator(); + + FoundryDeployConfigurationDocument result = generator.Generate(new FoundryConfigurationDocument + { + OsRecovery = new OsRecoverySettings + { + IsEnabled = true + } + }); + + Assert.True(result.OsRecovery.IsEnabled); + } + + [Fact] + public void Generate_WhenOsRecoveryIsNotConfigured_DefaultsDeployOsRecoveryToDisabled() + { + var generator = new DeployConfigurationGenerator(); + + FoundryDeployConfigurationDocument result = generator.Generate(new FoundryConfigurationDocument()); + + Assert.False(result.OsRecovery.IsEnabled); + } + [Fact] public void Generate_PropagatesTelemetrySettings() { diff --git a/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs b/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs index 0d445a63..8420f040 100644 --- a/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs +++ b/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs @@ -22,6 +22,10 @@ public void Serialize_ThenDeserialize_RoundTripsBusinessSettings() var document = new FoundryConfigurationDocument { + OsRecovery = new OsRecoverySettings + { + IsEnabled = true + }, Network = new NetworkSettings { WifiProvisioned = true, @@ -97,6 +101,7 @@ public void Serialize_ThenDeserialize_RoundTripsBusinessSettings() string json = service.Serialize(document); FoundryConfigurationDocument loaded = service.Deserialize(json); + Assert.True(loaded.OsRecovery.IsEnabled); Assert.True(loaded.Network.WifiProvisioned); Assert.Equal("CorpWiFi", loaded.Network.Wifi.Ssid); Assert.True(loaded.OperatingSystemSelection.IsEnabled); diff --git a/src/Foundry.Core.Tests/WinPe/OsRecoveryPayloadProvisioningServiceTests.cs b/src/Foundry.Core.Tests/WinPe/OsRecoveryPayloadProvisioningServiceTests.cs new file mode 100644 index 00000000..7870338d --- /dev/null +++ b/src/Foundry.Core.Tests/WinPe/OsRecoveryPayloadProvisioningServiceTests.cs @@ -0,0 +1,402 @@ +using System.IO.Compression; +using System.Text; +using System.Xml.Linq; +using Foundry.Core.Services.Configuration; +using Foundry.Core.Services.WinPe; +using Foundry.Core.Services.WinPe.OsRecovery; + +namespace Foundry.Core.Tests.WinPe; + +public sealed class OsRecoveryPayloadProvisioningServiceTests +{ + [Fact] + public async Task ProvisionAsync_StagesRequiredFilesAndExcludesWinPeOnlyArtifacts() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", "connect"); + + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{\"schemaVersion\":1}", + DeployConfigurationJson = "{\"schemaVersion\":2}", + IanaWindowsTimeZoneMapJson = "{\"zones\":[]}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = CreateBootMenuLocalizations() + }, + cancellationToken: CancellationToken.None); + + Assert.True(result.IsSuccess, result.Error?.Details); + Assert.NotNull(result.Value); + + string toolsPath = Path.Combine(workspace.MountedImagePath, "Sources", "Recovery", "Tools"); + string system32Path = Path.Combine(workspace.MountedImagePath, "Windows", "System32"); + string configPath = Path.Combine(workspace.MountedImagePath, "Foundry", "Config"); + string connectRuntimePath = Path.Combine(workspace.MountedImagePath, "Foundry", "Runtime", "Foundry.Connect", "win-x64"); + string sevenZipToolsPath = Path.Combine(workspace.MountedImagePath, "Foundry", "Tools", "7zip"); + + Assert.True(File.Exists(Path.Combine(toolsPath, "FoundryRecoveryLauncher.cmd"))); + Assert.True(File.Exists(Path.Combine(toolsPath, "WinREConfig.xml"))); + Assert.False(File.Exists(Path.Combine(system32Path, "FoundryBootstrap.ps1"))); + Assert.Equal("{\"schemaVersion\":1}", await File.ReadAllTextAsync(Path.Combine(configPath, "foundry.connect.config.json"))); + Assert.Equal("{\"schemaVersion\":2}", await File.ReadAllTextAsync(Path.Combine(configPath, "foundry.deploy.config.json"))); + Assert.Equal("{\"zones\":[]}", await File.ReadAllTextAsync(Path.Combine(configPath, "iana-windows-timezones.json"))); + Assert.Equal("curl", await File.ReadAllTextAsync(Path.Combine(system32Path, "curl.exe"))); + Assert.Equal("connect", await File.ReadAllTextAsync(Path.Combine(connectRuntimePath, "Foundry.Connect.exe"))); + Assert.Equal("7za", await File.ReadAllTextAsync(Path.Combine(sevenZipToolsPath, "x64", "7za.exe"))); + Assert.Equal("license", await File.ReadAllTextAsync(Path.Combine(sevenZipToolsPath, "License.txt"))); + Assert.Equal("readme", await File.ReadAllTextAsync(Path.Combine(sevenZipToolsPath, "readme.txt"))); + + Assert.False(Directory.Exists(Path.Combine(workspace.MountedImagePath, "Foundry", "Runtime", "Foundry.Deploy"))); + Assert.False(File.Exists(Path.Combine(configPath, "foundry.connect.provisioning-source.txt"))); + Assert.False(File.Exists(Path.Combine(configPath, "foundry.deploy.provisioning-source.txt"))); + Assert.False(Directory.Exists(Path.Combine(configPath, "Secrets"))); + Assert.False(Directory.Exists(Path.Combine(configPath, "Network"))); + Assert.False(Directory.Exists(Path.Combine(configPath, "Autopilot"))); + Assert.False(Directory.Exists(Path.Combine(workspace.MountedImagePath, "Foundry", "Runtime", "AutopilotHash"))); + Assert.False(Directory.Exists(Path.Combine(workspace.MountedImagePath, "Foundry", "Tools", "OA3"))); + } + + [Fact] + public async Task ProvisionAsync_WritesCmdOnlyRecoveryLauncher() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", "connect"); + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{}", + DeployConfigurationJson = "{}", + IanaWindowsTimeZoneMapJson = "{}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = CreateBootMenuLocalizations() + }, + cancellationToken: CancellationToken.None); + + Assert.True(result.IsSuccess, result.Error?.Details); + + string launcherPath = Path.Combine(workspace.MountedImagePath, "Sources", "Recovery", "Tools", "FoundryRecoveryLauncher.cmd"); + string launcher = await File.ReadAllTextAsync(launcherPath); + + Assert.Contains("FOUNDRY_DEPLOYMENT_MODE=Recovery", launcher, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Foundry.Connect.exe", launcher, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Foundry.Deploy-%RID%.zip", launcher, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Foundry.Deploy.exe", launcher, StringComparison.OrdinalIgnoreCase); + Assert.Contains("curl.exe", launcher, StringComparison.OrdinalIgnoreCase); + Assert.Contains("7za.exe", launcher, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("powershell", launcher, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("FoundryBootstrap.ps1", launcher, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ProvisionAsync_WritesWinReConfigXmlAsUtf8WithoutBom() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", "connect"); + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{}", + DeployConfigurationJson = "{}", + IanaWindowsTimeZoneMapJson = "{}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = CreateBootMenuLocalizations() + }, + cancellationToken: CancellationToken.None); + + Assert.True(result.IsSuccess, result.Error?.Details); + + string winReConfigPath = Path.Combine(workspace.MountedImagePath, "Sources", "Recovery", "Tools", "WinREConfig.xml"); + byte[] bytes = await File.ReadAllBytesAsync(winReConfigPath); + Assert.False(bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF); + + XDocument document = XDocument.Parse(Encoding.UTF8.GetString(bytes)); + Assert.Equal("Recovery", document.Root?.Name.LocalName); + Assert.Equal( + "FoundryRecoveryLauncher.cmd", + document.Root?.Element("RecoveryTools")?.Element("RelativeFilePath")?.Value); + } + + [Fact] + public async Task ProvisionAsync_ReturnsBootMenuConfigurationForEverySupportedCulture() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", "connect"); + IReadOnlyList localizations = CreateBootMenuLocalizations(); + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{}", + DeployConfigurationJson = "{}", + IanaWindowsTimeZoneMapJson = "{}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = localizations + }, + cancellationToken: CancellationToken.None); + + Assert.True(result.IsSuccess, result.Error?.Details); + + XDocument document = XDocument.Parse(result.Value!.BootMenuConfigurationXml); + XElement[] entries = document.Root!.Elements("WinRETool").ToArray(); + Assert.Equal(localizations.Count, entries.Length); + Assert.All(entries, entry => + { + Assert.False(string.IsNullOrWhiteSpace(entry.Attribute("locale")?.Value)); + Assert.False(string.IsNullOrWhiteSpace(entry.Element("Name")?.Value)); + Assert.False(string.IsNullOrWhiteSpace(entry.Element("Description")?.Value)); + }); + } + + [Fact] + public async Task ProvisionAsync_WhenBootMenuLocalizationIsMissingSupportedCulture_ReturnsFailure() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", "connect"); + List localizations = [.. CreateBootMenuLocalizations()]; + localizations.RemoveAt(localizations.Count - 1); + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{}", + DeployConfigurationJson = "{}", + IanaWindowsTimeZoneMapJson = "{}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = localizations + }, + cancellationToken: CancellationToken.None); + + Assert.False(result.IsSuccess); + Assert.Contains("supported culture", result.Error?.Details, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ProvisionAsync_WhenBootMenuTextExceedsThirtyCharacters_ReturnsFailure() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", "connect"); + List localizations = [.. CreateBootMenuLocalizations()]; + localizations[0] = localizations[0] with + { + Name = "Foundry OS Recovery Utility Name", + Description = "Foundry OS Recovery Utility Description" + }; + + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{}", + DeployConfigurationJson = "{}", + IanaWindowsTimeZoneMapJson = "{}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = localizations + }, + cancellationToken: CancellationToken.None); + + Assert.False(result.IsSuccess); + Assert.Contains("30 characters", result.Error?.Details, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ProvisionAsync_WhenManagedPayloadExceedsBudget_ReturnsFailure() + { + using TempOsRecoveryWorkspace workspace = TempOsRecoveryWorkspace.Create(); + string connectArchivePath = workspace.CreateArchive("connect.zip", "Foundry.Connect.exe", new string('c', 256)); + var service = new OsRecoveryPayloadProvisioningService( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService(new FakeRuntimeProcessRunner())); + + WinPeResult result = await service.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = workspace.MountedImagePath, + WorkingDirectoryPath = workspace.WorkingDirectoryPath, + Architecture = WinPeArchitecture.X64, + FoundryConnectConfigurationJson = "{}", + DeployConfigurationJson = "{}", + IanaWindowsTimeZoneMapJson = "{}", + CurlExecutableSourcePath = workspace.CurlExecutablePath, + SevenZipSourceDirectoryPath = workspace.SevenZipSourcePath, + MaxManagedPayloadSizeBytes = 32, + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ArchivePath = connectArchivePath + }, + BootMenuLocalizations = CreateBootMenuLocalizations() + }, + cancellationToken: CancellationToken.None); + + Assert.False(result.IsSuccess); + Assert.Contains("256 MiB", result.Error?.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("32", result.Error?.Details, StringComparison.OrdinalIgnoreCase); + } + + private static IReadOnlyList CreateBootMenuLocalizations() + { + return new EmbeddedLanguageRegistryService() + .GetLanguages() + .Select(language => new OsRecoveryBootMenuLocalization + { + Culture = language.Code, + Name = "Foundry Recovery", + Description = "Recover this device" + }) + .ToArray(); + } + + private sealed class FakeRuntimeProcessRunner : IWinPeProcessRunner + { + public Task RunAsync( + string fileName, + string arguments, + string workingDirectory, + CancellationToken cancellationToken, + IReadOnlyDictionary? environmentOverrides = null) + { + throw new NotSupportedException(); + } + + public Task RunCmdScriptAsync( + string scriptPath, + string scriptArguments, + string workingDirectory, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public Task RunCmdScriptDirectAsync( + string scriptPath, + string scriptArguments, + string workingDirectory, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + } + + private sealed class TempOsRecoveryWorkspace : IDisposable + { + private TempOsRecoveryWorkspace(string rootPath) + { + RootPath = rootPath; + MountedImagePath = Path.Combine(rootPath, "mount"); + WorkingDirectoryPath = Path.Combine(rootPath, "work"); + SevenZipSourcePath = Path.Combine(rootPath, "7z"); + CurlExecutablePath = Path.Combine(rootPath, "curl.exe"); + + Directory.CreateDirectory(MountedImagePath); + Directory.CreateDirectory(WorkingDirectoryPath); + Directory.CreateDirectory(Path.Combine(SevenZipSourcePath, "x64")); + File.WriteAllText(CurlExecutablePath, "curl"); + File.WriteAllText(Path.Combine(SevenZipSourcePath, "x64", "7za.exe"), "7za"); + File.WriteAllText(Path.Combine(SevenZipSourcePath, "License.txt"), "license"); + File.WriteAllText(Path.Combine(SevenZipSourcePath, "readme.txt"), "readme"); + } + + public string RootPath { get; } + public string MountedImagePath { get; } + public string WorkingDirectoryPath { get; } + public string SevenZipSourcePath { get; } + public string CurlExecutablePath { get; } + + public static TempOsRecoveryWorkspace Create() + { + return new TempOsRecoveryWorkspace(Path.Combine(Path.GetTempPath(), $"foundry-osrecovery-{Guid.NewGuid():N}")); + } + + public string CreateArchive(string archiveName, string executableName, string content) + { + string payloadPath = Path.Combine(RootPath, "payloads", Path.GetFileNameWithoutExtension(archiveName)); + Directory.CreateDirectory(payloadPath); + File.WriteAllText(Path.Combine(payloadPath, executableName), content); + + string archivePath = Path.Combine(RootPath, archiveName); + ZipFile.CreateFromDirectory(payloadPath, archivePath); + return archivePath; + } + + public void Dispose() + { + Directory.Delete(RootPath, recursive: true); + } + } +} diff --git a/src/Foundry.Core/Assets/WinPe/FoundryBootstrap.ps1 b/src/Foundry.Core/Assets/WinPe/FoundryBootstrap.ps1 index ba03da75..40a61a1b 100644 --- a/src/Foundry.Core/Assets/WinPe/FoundryBootstrap.ps1 +++ b/src/Foundry.Core/Assets/WinPe/FoundryBootstrap.ps1 @@ -1844,16 +1844,39 @@ try { Write-ConsoleSection -Title 'Runtime' - # USB cache media takes precedence; otherwise the ISO-backed runtime directory is used. - $usbRuntimeRoot = Get-UsbCacheRuntimeRoot - if (-not [string]::IsNullOrWhiteSpace($usbRuntimeRoot)) { - $bootstrapRoot = $usbRuntimeRoot - $deploymentMode = 'Usb' + $requestedDeploymentMode = [string]$env:FOUNDRY_DEPLOYMENT_MODE + $requestedDeploymentMode = $requestedDeploymentMode.Trim() + + if ($requestedDeploymentMode -ieq 'Recovery') { + $bootstrapRoot = Join-Path $WinPeRoot 'Runtime' + $deploymentMode = 'Recovery' } - else { + elseif ($requestedDeploymentMode -ieq 'Iso') { $bootstrapRoot = Join-Path $WinPeRoot 'Runtime' $deploymentMode = 'Iso' } + elseif ($requestedDeploymentMode -ieq 'Usb') { + $usbRuntimeRoot = Get-UsbCacheRuntimeRoot + $bootstrapRoot = if ([string]::IsNullOrWhiteSpace($usbRuntimeRoot)) { + Join-Path $WinPeRoot 'Runtime' + } + else { + $usbRuntimeRoot + } + $deploymentMode = 'Usb' + } + else { + # USB cache media takes precedence; otherwise the ISO-backed runtime directory is used. + $usbRuntimeRoot = Get-UsbCacheRuntimeRoot + if (-not [string]::IsNullOrWhiteSpace($usbRuntimeRoot)) { + $bootstrapRoot = $usbRuntimeRoot + $deploymentMode = 'Usb' + } + else { + $bootstrapRoot = Join-Path $WinPeRoot 'Runtime' + $deploymentMode = 'Iso' + } + } Ensure-Directory -Path $bootstrapRoot @@ -1945,8 +1968,8 @@ try { } elseif ($deploymentMode -ne 'Usb') { Write-Log ` - 'Skipping Foundry.Connect cache verification because the deployment mode is ISO.' ` - -ConsoleMessage 'Foundry.Connect: update check skipped in ISO mode.' + "Skipping Foundry.Connect cache verification because the deployment mode is $deploymentMode." ` + -ConsoleMessage "Foundry.Connect: update check skipped in $deploymentMode mode." } else { try { diff --git a/src/Foundry.Core/Assets/WinRe/FoundryRecoveryLauncher.cmd b/src/Foundry.Core/Assets/WinRe/FoundryRecoveryLauncher.cmd new file mode 100644 index 00000000..330c543b --- /dev/null +++ b/src/Foundry.Core/Assets/WinRe/FoundryRecoveryLauncher.cmd @@ -0,0 +1,99 @@ +@echo off +setlocal + +set "FOUNDRY_DEPLOYMENT_MODE=Recovery" +set "FOUNDRY_ROOT=X:\Foundry" +set "RUNTIME_ROOT=%FOUNDRY_ROOT%\Runtime" +set "CONFIG_ROOT=%FOUNDRY_ROOT%\Config" +set "TOOLS_ROOT=%FOUNDRY_ROOT%\Tools" +set "LOG_ROOT=%FOUNDRY_ROOT%\Logs" +set "LOG_PATH=%LOG_ROOT%\FoundryRecoveryLauncher.log" + +if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "RID=win-arm64" + set "SEVENZIP_RID=arm64" +) else ( + set "RID=win-x64" + set "SEVENZIP_RID=x64" +) + +set "CONNECT_ROOT=%RUNTIME_ROOT%\Foundry.Connect\%RID%" +set "CONNECT_EXE=%CONNECT_ROOT%\Foundry.Connect.exe" +set "CONNECT_CONFIG=%CONFIG_ROOT%\foundry.connect.config.json" +set "DEPLOY_ROOT=%RUNTIME_ROOT%\Foundry.Deploy\%RID%" +set "DEPLOY_ARCHIVE=%RUNTIME_ROOT%\Foundry.Deploy\Foundry.Deploy-%RID%.zip" +set "DEPLOY_EXE=%DEPLOY_ROOT%\Foundry.Deploy.exe" +set "DEPLOY_URL=https://github.com/foundry-osd/foundry/releases/latest/download/Foundry.Deploy-%RID%.zip" +set "CURL_EXE=%SystemRoot%\System32\curl.exe" +set "SEVENZIP_EXE=%TOOLS_ROOT%\7zip\%SEVENZIP_RID%\7za.exe" + +mkdir "%LOG_ROOT%" >nul 2>&1 +mkdir "%RUNTIME_ROOT%\Foundry.Deploy" >nul 2>&1 +echo [%date% %time%] Starting Foundry OS Recovery launcher.>"%LOG_PATH%" +echo [%date% %time%] Runtime identifier: %RID%.>>"%LOG_PATH%" + +if not exist "%CONNECT_EXE%" ( + echo Missing Foundry.Connect runtime: %CONNECT_EXE% + echo [%date% %time%] Missing Foundry.Connect runtime: %CONNECT_EXE%.>>"%LOG_PATH%" + exit /b 1 +) + +if not exist "%CURL_EXE%" ( + echo Missing curl.exe: %CURL_EXE% + echo [%date% %time%] Missing curl.exe: %CURL_EXE%.>>"%LOG_PATH%" + exit /b 1 +) + +if not exist "%SEVENZIP_EXE%" ( + echo Missing 7-Zip runtime: %SEVENZIP_EXE% + echo [%date% %time%] Missing 7-Zip runtime: %SEVENZIP_EXE%.>>"%LOG_PATH%" + exit /b 1 +) + +echo Starting Foundry.Connect... +echo [%date% %time%] Starting Foundry.Connect.>>"%LOG_PATH%" +if exist "%CONNECT_CONFIG%" ( + start /wait "" /d "%CONNECT_ROOT%" "%CONNECT_EXE%" --config "%CONNECT_CONFIG%" +) else ( + start /wait "" /d "%CONNECT_ROOT%" "%CONNECT_EXE%" +) + +set "CONNECT_EXIT=%ERRORLEVEL%" +echo [%date% %time%] Foundry.Connect exited with %CONNECT_EXIT%.>>"%LOG_PATH%" +if not "%CONNECT_EXIT%"=="0" ( + exit /b %CONNECT_EXIT% +) + +if not exist "%DEPLOY_EXE%" ( + echo Downloading Foundry.Deploy... + echo [%date% %time%] Downloading Foundry.Deploy from %DEPLOY_URL%.>>"%LOG_PATH%" + "%CURL_EXE%" --fail --location --show-error --output "%DEPLOY_ARCHIVE%" --url "%DEPLOY_URL%" >>"%LOG_PATH%" 2>&1 + if errorlevel 1 ( + echo Foundry.Deploy download failed. + echo [%date% %time%] Foundry.Deploy download failed.>>"%LOG_PATH%" + exit /b 1 + ) + + if exist "%DEPLOY_ROOT%" rd /s /q "%DEPLOY_ROOT%" + mkdir "%DEPLOY_ROOT%" >nul 2>&1 + echo Extracting Foundry.Deploy... + echo [%date% %time%] Extracting Foundry.Deploy.>>"%LOG_PATH%" + "%SEVENZIP_EXE%" x -y "%DEPLOY_ARCHIVE%" "-o%DEPLOY_ROOT%" >>"%LOG_PATH%" 2>&1 + if errorlevel 1 ( + echo Foundry.Deploy extraction failed. + echo [%date% %time%] Foundry.Deploy extraction failed.>>"%LOG_PATH%" + exit /b 1 + ) +) + +if not exist "%DEPLOY_EXE%" ( + echo Missing Foundry.Deploy runtime: %DEPLOY_EXE% + echo [%date% %time%] Missing Foundry.Deploy runtime after download: %DEPLOY_EXE%.>>"%LOG_PATH%" + exit /b 1 +) + +echo Starting Foundry.Deploy... +echo [%date% %time%] Starting Foundry.Deploy.>>"%LOG_PATH%" +start "" /d "%DEPLOY_ROOT%" "%DEPLOY_EXE%" + +exit /b %ERRORLEVEL% diff --git a/src/Foundry.Core/Foundry.Core.csproj b/src/Foundry.Core/Foundry.Core.csproj index e5f1a17a..6184da78 100644 --- a/src/Foundry.Core/Foundry.Core.csproj +++ b/src/Foundry.Core/Foundry.Core.csproj @@ -20,6 +20,9 @@ Foundry.Core.WinPe.ProvisionUsbDisk + + Foundry.Core.WinRe.FoundryRecoveryLauncher + diff --git a/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs b/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs index 88b34bc0..bbee6d01 100644 --- a/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs +++ b/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs @@ -2,11 +2,11 @@ namespace Foundry.Core.Models.Configuration; public static class ConfigurationSchemaVersions { - public const int FoundryCurrent = 10; + public const int FoundryCurrent = 11; public const int ConnectCurrent = 2; - public const int DeployCurrent = 8; + public const int DeployCurrent = 9; public static bool IsBootMediaUpdateRecommended(int schemaVersion, int currentSchemaVersion) { diff --git a/src/Foundry.Core/Models/Configuration/Deploy/DeployOsRecoverySettings.cs b/src/Foundry.Core/Models/Configuration/Deploy/DeployOsRecoverySettings.cs new file mode 100644 index 00000000..1b7cfbca --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/Deploy/DeployOsRecoverySettings.cs @@ -0,0 +1,12 @@ +namespace Foundry.Core.Models.Configuration.Deploy; + +/// +/// Describes whether Foundry.Deploy should expect Windows RE OS recovery integration. +/// +public sealed record DeployOsRecoverySettings +{ + /// + /// Gets a value indicating whether the OS recovery integration is enabled. + /// + public bool IsEnabled { get; init; } +} diff --git a/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs b/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs index c0589be7..1087ab7d 100644 --- a/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs +++ b/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs @@ -28,6 +28,11 @@ public sealed record FoundryDeployConfigurationDocument /// public DeployLocalizationSettings Localization { get; init; } = new(); + /// + /// Gets Windows RE OS recovery settings used during deployment. + /// + public DeployOsRecoverySettings OsRecovery { get; init; } = new(); + /// /// Gets network profile roaming settings used during deployment. /// diff --git a/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs b/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs index 96e88b59..4b7329e9 100644 --- a/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs +++ b/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs @@ -37,6 +37,11 @@ public sealed record FoundryConfigurationDocument /// public LocalizationSettings Localization { get; init; } = new(); + /// + /// Gets user-authored Windows RE OS recovery settings. + /// + public OsRecoverySettings OsRecovery { get; init; } = new(); + /// /// Gets user-authored customization settings used when deployment configuration is generated. /// diff --git a/src/Foundry.Core/Models/Configuration/OsRecoverySettings.cs b/src/Foundry.Core/Models/Configuration/OsRecoverySettings.cs new file mode 100644 index 00000000..d226cc18 --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/OsRecoverySettings.cs @@ -0,0 +1,12 @@ +namespace Foundry.Core.Models.Configuration; + +/// +/// Describes whether Foundry should prepare the Windows Recovery Environment custom tool payload. +/// +public sealed record OsRecoverySettings +{ + /// + /// Gets a value indicating whether the OS recovery payload should be generated. + /// + public bool IsEnabled { get; init; } +} diff --git a/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs b/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs index 0edb6a40..90ccabbd 100644 --- a/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs +++ b/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs @@ -35,6 +35,10 @@ public FoundryDeployConfigurationDocument Generate(FoundryConfigurationDocument { DefaultTimeZoneId = document.Localization.DefaultTimeZoneId }, + OsRecovery = new DeployOsRecoverySettings + { + IsEnabled = document.OsRecovery.IsEnabled + }, Network = new DeployNetworkSettings { ProfileRoaming = new DeployNetworkProfileRoamingSettings diff --git a/src/Foundry.Core/Services/WinPe/OsRecovery/IOsRecoveryPayloadProvisioningService.cs b/src/Foundry.Core/Services/WinPe/OsRecovery/IOsRecoveryPayloadProvisioningService.cs new file mode 100644 index 00000000..9b097671 --- /dev/null +++ b/src/Foundry.Core/Services/WinPe/OsRecovery/IOsRecoveryPayloadProvisioningService.cs @@ -0,0 +1,9 @@ +namespace Foundry.Core.Services.WinPe.OsRecovery; + +public interface IOsRecoveryPayloadProvisioningService +{ + Task> ProvisionAsync( + OsRecoveryPayloadProvisioningOptions options, + IProgress? downloadProgress = null, + CancellationToken cancellationToken = default); +} diff --git a/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryBootMenuLocalization.cs b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryBootMenuLocalization.cs new file mode 100644 index 00000000..eb88ad8a --- /dev/null +++ b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryBootMenuLocalization.cs @@ -0,0 +1,8 @@ +namespace Foundry.Core.Services.WinPe.OsRecovery; + +public sealed record OsRecoveryBootMenuLocalization +{ + public string Culture { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; +} diff --git a/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningOptions.cs b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningOptions.cs new file mode 100644 index 00000000..318a38b0 --- /dev/null +++ b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningOptions.cs @@ -0,0 +1,18 @@ +namespace Foundry.Core.Services.WinPe.OsRecovery; + +public sealed record OsRecoveryPayloadProvisioningOptions +{ + public const long DefaultManagedPayloadSizeBytes = 256L * 1024L * 1024L; + + public string MountedImagePath { get; init; } = string.Empty; + public string WorkingDirectoryPath { get; init; } = string.Empty; + public WinPeArchitecture Architecture { get; init; } = WinPeArchitecture.X64; + public string FoundryConnectConfigurationJson { get; init; } = string.Empty; + public string DeployConfigurationJson { get; init; } = string.Empty; + public string IanaWindowsTimeZoneMapJson { get; init; } = string.Empty; + public string CurlExecutableSourcePath { get; init; } = string.Empty; + public string SevenZipSourceDirectoryPath { get; init; } = string.Empty; + public WinPeRuntimePayloadApplicationOptions Connect { get; init; } = new(); + public IReadOnlyList BootMenuLocalizations { get; init; } = []; + public long MaxManagedPayloadSizeBytes { get; init; } = DefaultManagedPayloadSizeBytes; +} diff --git a/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningResult.cs b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningResult.cs new file mode 100644 index 00000000..eef53bc2 --- /dev/null +++ b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningResult.cs @@ -0,0 +1,7 @@ +namespace Foundry.Core.Services.WinPe.OsRecovery; + +public sealed record OsRecoveryPayloadProvisioningResult +{ + public required string BootMenuConfigurationXml { get; init; } + public required long ManagedPayloadSizeBytes { get; init; } +} diff --git a/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningService.cs b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningService.cs new file mode 100644 index 00000000..99dd7319 --- /dev/null +++ b/src/Foundry.Core/Services/WinPe/OsRecovery/OsRecoveryPayloadProvisioningService.cs @@ -0,0 +1,381 @@ +using System.Reflection; +using System.Text; +using System.Xml.Linq; +using Foundry.Core.Services.Configuration; + +namespace Foundry.Core.Services.WinPe.OsRecovery; + +public sealed class OsRecoveryPayloadProvisioningService : IOsRecoveryPayloadProvisioningService +{ + private const string LauncherResourceName = "Foundry.Core.WinRe.FoundryRecoveryLauncher"; + private const string LauncherFileName = "FoundryRecoveryLauncher.cmd"; + private const string WinReConfigFileName = "WinREConfig.xml"; + private static readonly UTF8Encoding Utf8NoBom = new(false); + + private readonly ILanguageRegistryService _languageRegistryService; + private readonly IWinPeRuntimePayloadProvisioningService _runtimePayloadProvisioningService; + + public OsRecoveryPayloadProvisioningService() + : this( + new EmbeddedLanguageRegistryService(), + new WinPeRuntimePayloadProvisioningService()) + { + } + + internal OsRecoveryPayloadProvisioningService( + ILanguageRegistryService languageRegistryService, + IWinPeRuntimePayloadProvisioningService runtimePayloadProvisioningService) + { + _languageRegistryService = languageRegistryService; + _runtimePayloadProvisioningService = runtimePayloadProvisioningService; + } + + public async Task> ProvisionAsync( + OsRecoveryPayloadProvisioningOptions options, + IProgress? downloadProgress = null, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + WinPeDiagnostic? validationError = ValidateOptions(options); + if (validationError is not null) + { + return WinPeResult.Failure(validationError); + } + + try + { + string mountedImagePath = Path.GetFullPath(options.MountedImagePath); + string recoveryToolsPath = Path.Combine(mountedImagePath, "Sources", "Recovery", "Tools"); + string system32Path = Path.Combine(mountedImagePath, "Windows", "System32"); + string foundryConfigPath = Path.Combine(mountedImagePath, "Foundry", "Config"); + + Directory.CreateDirectory(recoveryToolsPath); + Directory.CreateDirectory(system32Path); + Directory.CreateDirectory(foundryConfigPath); + + string launcherContent = LoadLauncherContent(); + string winReConfigXml = CreateWinReConfigurationXml(); + string bootMenuConfigurationXml = CreateBootMenuConfigurationXml(options.BootMenuLocalizations); + + await File.WriteAllTextAsync( + Path.Combine(recoveryToolsPath, LauncherFileName), + launcherContent, + Utf8NoBom, + cancellationToken).ConfigureAwait(false); + await File.WriteAllTextAsync( + Path.Combine(recoveryToolsPath, WinReConfigFileName), + winReConfigXml, + Utf8NoBom, + cancellationToken).ConfigureAwait(false); + await File.WriteAllTextAsync( + Path.Combine(foundryConfigPath, "foundry.connect.config.json"), + options.FoundryConnectConfigurationJson, + Utf8NoBom, + cancellationToken).ConfigureAwait(false); + await File.WriteAllTextAsync( + Path.Combine(foundryConfigPath, "foundry.deploy.config.json"), + options.DeployConfigurationJson, + Utf8NoBom, + cancellationToken).ConfigureAwait(false); + await File.WriteAllTextAsync( + Path.Combine(foundryConfigPath, "iana-windows-timezones.json"), + options.IanaWindowsTimeZoneMapJson, + Utf8NoBom, + cancellationToken).ConfigureAwait(false); + + File.Copy(options.CurlExecutableSourcePath, Path.Combine(system32Path, "curl.exe"), overwrite: true); + + ProvisionBundledSevenZip(mountedImagePath, options); + + WinPeResult runtimeProvisioningResult = await _runtimePayloadProvisioningService.ProvisionAsync( + new WinPeRuntimePayloadProvisioningOptions + { + Architecture = options.Architecture, + WorkingDirectoryPath = options.WorkingDirectoryPath, + MountedImagePath = mountedImagePath, + Connect = options.Connect + }, + downloadProgress, + cancellationToken).ConfigureAwait(false); + + if (!runtimeProvisioningResult.IsSuccess) + { + return WinPeResult.Failure(runtimeProvisioningResult.Error!); + } + + long managedPayloadSizeBytes = CalculateManagedPayloadSizeBytes(mountedImagePath, options.Architecture); + if (managedPayloadSizeBytes > options.MaxManagedPayloadSizeBytes) + { + return WinPeResult.Failure( + WinPeErrorCodes.ValidationFailed, + "Foundry OS recovery managed payload exceeds the default 256 MiB size budget.", + $"Managed payload size is {managedPayloadSizeBytes} bytes. Configured budget is {options.MaxManagedPayloadSizeBytes} bytes."); + } + + return WinPeResult.Success(new OsRecoveryPayloadProvisioningResult + { + BootMenuConfigurationXml = bootMenuConfigurationXml, + ManagedPayloadSizeBytes = managedPayloadSizeBytes + }); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or InvalidOperationException) + { + return WinPeResult.Failure( + WinPeErrorCodes.BuildFailed, + "Failed to provision the Foundry OS recovery payload.", + ex.Message); + } + } + + private string CreateBootMenuConfigurationXml(IReadOnlyList localizations) + { + IReadOnlyList supportedCultures = _languageRegistryService + .GetLanguages() + .Select(language => LanguageCodeUtility.Canonicalize(language.Code)) + .ToArray(); + + Dictionary localizationMap = localizations + .GroupBy(localization => LanguageCodeUtility.Canonicalize(localization.Culture), StringComparer.OrdinalIgnoreCase) + .ToDictionary( + group => group.Key, + group => + { + if (group.Count() > 1) + { + throw new ArgumentException($"Duplicate OS recovery boot menu localization was provided for culture '{group.Key}'."); + } + + return group.Single() with + { + Culture = group.Key + }; + }, + StringComparer.OrdinalIgnoreCase); + + foreach (string culture in supportedCultures) + { + if (!localizationMap.ContainsKey(culture)) + { + throw new ArgumentException($"An OS recovery boot menu localization is required for every supported culture. Missing culture: '{culture}'."); + } + } + + XElement[] entries = supportedCultures + .Select(culture => CreateBootMenuEntry(localizationMap[culture])) + .ToArray(); + + var document = new XDocument( + new XDeclaration("1.0", "utf-8", null), + new XElement("BootShell", entries)); + + return document.ToString(SaveOptions.DisableFormatting); + } + + private static XElement CreateBootMenuEntry(OsRecoveryBootMenuLocalization localization) + { + string name = localization.Name.Trim(); + string description = localization.Description.Trim(); + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"OS recovery boot menu name is required for culture '{localization.Culture}'."); + } + + if (string.IsNullOrWhiteSpace(description)) + { + throw new ArgumentException($"OS recovery boot menu description is required for culture '{localization.Culture}'."); + } + + if (name.Length > 30 || description.Length > 30) + { + throw new ArgumentException( + $"OS recovery boot menu name and description must not exceed 30 characters for culture '{localization.Culture}'."); + } + + return new XElement( + "WinRETool", + new XAttribute("locale", LanguageCodeUtility.Canonicalize(localization.Culture).ToLowerInvariant()), + new XElement("Name", name), + new XElement("Description", description)); + } + + private static string CreateWinReConfigurationXml() + { + var document = new XDocument( + new XDeclaration("1.0", "utf-8", null), + new XElement( + "Recovery", + new XElement( + "RecoveryTools", + new XElement("RelativeFilePath", LauncherFileName)))); + + return document.ToString(SaveOptions.DisableFormatting); + } + + private static long CalculateManagedPayloadSizeBytes(string mountedImagePath, WinPeArchitecture architecture) + { + string runtimeIdentifier = architecture.ToDotnetRuntimeIdentifier(); + string sevenZipRuntimeFolder = architecture.ToSevenZipRuntimeFolder(); + string[] managedPaths = + [ + Path.Combine(mountedImagePath, "Sources", "Recovery", "Tools", LauncherFileName), + Path.Combine(mountedImagePath, "Sources", "Recovery", "Tools", WinReConfigFileName), + Path.Combine(mountedImagePath, "Windows", "System32", "curl.exe"), + Path.Combine(mountedImagePath, "Foundry", "Config", "foundry.connect.config.json"), + Path.Combine(mountedImagePath, "Foundry", "Config", "foundry.deploy.config.json"), + Path.Combine(mountedImagePath, "Foundry", "Config", "iana-windows-timezones.json"), + Path.Combine(mountedImagePath, "Foundry", "Runtime", "Foundry.Connect", runtimeIdentifier), + Path.Combine(mountedImagePath, "Foundry", "Tools", "7zip", sevenZipRuntimeFolder), + Path.Combine(mountedImagePath, "Foundry", "Tools", "7zip", "License.txt"), + Path.Combine(mountedImagePath, "Foundry", "Tools", "7zip", "readme.txt") + ]; + + return managedPaths.Sum(CalculatePathSizeBytes); + } + + private static long CalculatePathSizeBytes(string path) + { + if (File.Exists(path)) + { + return new FileInfo(path).Length; + } + + if (!Directory.Exists(path)) + { + return 0; + } + + return Directory + .EnumerateFiles(path, "*", SearchOption.AllDirectories) + .Sum(filePath => new FileInfo(filePath).Length); + } + + private static void ProvisionBundledSevenZip(string mountedImagePath, OsRecoveryPayloadProvisioningOptions options) + { + string runtimeFolder = options.Architecture.ToSevenZipRuntimeFolder(); + string sourceRootPath = options.SevenZipSourceDirectoryPath; + string sourceExecutablePath = Path.Combine(sourceRootPath, runtimeFolder, "7za.exe"); + string sourceLicensePath = Path.Combine(sourceRootPath, "License.txt"); + string sourceReadmePath = Path.Combine(sourceRootPath, "readme.txt"); + + if (!File.Exists(sourceExecutablePath) || !File.Exists(sourceLicensePath) || !File.Exists(sourceReadmePath)) + { + throw new IOException($"Bundled 7-Zip assets are incomplete under '{sourceRootPath}' for runtime '{runtimeFolder}'."); + } + + string destinationToolsRootPath = Path.Combine(mountedImagePath, "Foundry", "Tools", "7zip"); + string destinationRuntimePath = Path.Combine(destinationToolsRootPath, runtimeFolder); + Directory.CreateDirectory(destinationRuntimePath); + + File.Copy(sourceExecutablePath, Path.Combine(destinationRuntimePath, "7za.exe"), overwrite: true); + File.Copy(sourceLicensePath, Path.Combine(destinationToolsRootPath, "License.txt"), overwrite: true); + File.Copy(sourceReadmePath, Path.Combine(destinationToolsRootPath, "readme.txt"), overwrite: true); + } + + private static string LoadLauncherContent() + { + Assembly assembly = typeof(OsRecoveryPayloadProvisioningService).Assembly; + using Stream? stream = assembly.GetManifestResourceStream(LauncherResourceName); + if (stream is null) + { + throw new InvalidOperationException($"Embedded recovery launcher resource '{LauncherResourceName}' was not found."); + } + + using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + return reader.ReadToEnd(); + } + + private static WinPeDiagnostic? ValidateOptions(OsRecoveryPayloadProvisioningOptions? options) + { + if (options is null) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "OS recovery payload provisioning options are required.", + "Provide a non-null OsRecoveryPayloadProvisioningOptions instance."); + } + + if (string.IsNullOrWhiteSpace(options.MountedImagePath)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Mounted image path is required for OS recovery payload provisioning.", + "Set OsRecoveryPayloadProvisioningOptions.MountedImagePath."); + } + + if (string.IsNullOrWhiteSpace(options.WorkingDirectoryPath)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Working directory path is required for OS recovery payload provisioning.", + "Set OsRecoveryPayloadProvisioningOptions.WorkingDirectoryPath."); + } + + if (!Enum.IsDefined(options.Architecture)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "WinPE architecture value is invalid.", + $"Value: '{options.Architecture}'."); + } + + if (string.IsNullOrWhiteSpace(options.FoundryConnectConfigurationJson)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Foundry.Connect configuration JSON is required for OS recovery payload provisioning.", + "Set OsRecoveryPayloadProvisioningOptions.FoundryConnectConfigurationJson."); + } + + if (string.IsNullOrWhiteSpace(options.DeployConfigurationJson)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Foundry.Deploy configuration JSON is required for OS recovery payload provisioning.", + "Set OsRecoveryPayloadProvisioningOptions.DeployConfigurationJson."); + } + + if (string.IsNullOrWhiteSpace(options.IanaWindowsTimeZoneMapJson)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "IANA Windows time zone map JSON is required for OS recovery payload provisioning.", + "Set OsRecoveryPayloadProvisioningOptions.IanaWindowsTimeZoneMapJson."); + } + + if (string.IsNullOrWhiteSpace(options.SevenZipSourceDirectoryPath)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Bundled 7-Zip source directory is required for OS recovery payload provisioning.", + "Set OsRecoveryPayloadProvisioningOptions.SevenZipSourceDirectoryPath."); + } + + if (string.IsNullOrWhiteSpace(options.CurlExecutableSourcePath) || !File.Exists(options.CurlExecutableSourcePath)) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "curl.exe source path is required for OS recovery payload provisioning.", + $"Expected file: '{options.CurlExecutableSourcePath}'."); + } + + if (!options.Connect.IsEnabled) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Foundry.Connect runtime payload provisioning must be enabled for OS recovery.", + "Set OsRecoveryPayloadProvisioningOptions.Connect.IsEnabled to true."); + } + + if (options.MaxManagedPayloadSizeBytes <= 0) + { + return new WinPeDiagnostic( + WinPeErrorCodes.ValidationFailed, + "Managed payload size budget must be greater than zero.", + $"Configured budget: '{options.MaxManagedPayloadSizeBytes}'."); + } + + return null; + } +} diff --git a/src/Foundry.Deploy.Tests/DeploymentLaunchPreparationServiceTests.cs b/src/Foundry.Deploy.Tests/DeploymentLaunchPreparationServiceTests.cs index 5b8f1fab..9ad1f69e 100644 --- a/src/Foundry.Deploy.Tests/DeploymentLaunchPreparationServiceTests.cs +++ b/src/Foundry.Deploy.Tests/DeploymentLaunchPreparationServiceTests.cs @@ -92,6 +92,10 @@ public void Prepare_WhenRequestIsValidAndConfirmed_ReturnsDeploymentContext() RemoveCopilot = true, DisableRecall = true }; + DeployOsRecoverySettings osRecovery = new() + { + IsEnabled = true + }; DeploymentLaunchPreparationResult result = service.Prepare( CreateRequest( @@ -102,6 +106,7 @@ public void Prepare_WhenRequestIsValidAndConfirmed_ReturnsDeploymentContext() defaultTimeZoneId: " Romance Standard Time ", isAutopilotEnabled: true, selectedAutopilotProfile: autopilotProfile, + osRecovery: osRecovery, oobe: oobe, appxRemoval: appxRemoval, aiComponentRemoval: aiComponentRemoval)); @@ -114,6 +119,7 @@ public void Prepare_WhenRequestIsValidAndConfirmed_ReturnsDeploymentContext() Assert.Equal("Romance Standard Time", result.Context?.DefaultTimeZoneId); Assert.Same(driverPack, result.Context?.DriverPack); Assert.Same(autopilotProfile, result.Context?.SelectedAutopilotProfile); + Assert.Same(osRecovery, result.Context?.OsRecovery); Assert.Same(oobe, result.Context?.Oobe); Assert.Same(appxRemoval, result.Context?.AppxRemoval); Assert.Same(aiComponentRemoval, result.Context?.AiComponentRemoval); @@ -235,8 +241,39 @@ public void Prepare_WhenConfirmationIsShown_UsesLocalizedWarningText() } } + [Fact] + public void Prepare_WhenRecoveryModeIsUsed_ShowsRecoverySpecificWarningText() + { + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo originalUiCulture = CultureInfo.CurrentUICulture; + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US"); + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); + + try + { + var shell = new FakeApplicationShellService { ConfirmationResult = true }; + var service = new DeploymentLaunchPreparationService(shell); + + DeploymentLaunchPreparationResult result = service.Prepare(CreateRequest( + selectedTargetDisk: CreateDisk(), + mode: DeploymentMode.Recovery)); + + Assert.True(result.IsReadyToStart); + Assert.Equal("Confirm OS Recovery", shell.LastConfirmationTitle); + Assert.Contains("preserve EFI, MSR, and Recovery partitions", shell.LastConfirmationMessage); + Assert.Contains("replace the Windows partition", shell.LastConfirmationMessage); + Assert.Contains("Continue with OS Recovery?", shell.LastConfirmationMessage); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + CultureInfo.CurrentUICulture = originalUiCulture; + } + } + private static DeploymentLaunchRequest CreateRequest( TargetDiskInfo? selectedTargetDisk, + DeploymentMode mode = DeploymentMode.Usb, string targetComputerName = "LAB-01", string? defaultTimeZoneId = null, DriverPackSelectionKind driverPackSelectionKind = DriverPackSelectionKind.None, @@ -245,6 +282,7 @@ private static DeploymentLaunchRequest CreateRequest( AutopilotProvisioningMode autopilotProvisioningMode = AutopilotProvisioningMode.JsonProfile, AutopilotProfileCatalogItem? selectedAutopilotProfile = null, DeployAutopilotHardwareHashUploadSettings? autopilotHardwareHashUpload = null, + DeployOsRecoverySettings? osRecovery = null, DeployOobeSettings? oobe = null, DeployAppxRemovalSettings? appxRemoval = null, DeployAiComponentRemovalSettings? aiComponentRemoval = null, @@ -252,7 +290,7 @@ private static DeploymentLaunchRequest CreateRequest( { return new DeploymentLaunchRequest { - Mode = DeploymentMode.Usb, + Mode = mode, CacheRootPath = @"X:\Foundry\Runtime", TargetComputerName = targetComputerName, DefaultTimeZoneId = defaultTimeZoneId, @@ -275,6 +313,7 @@ private static DeploymentLaunchRequest CreateRequest( AutopilotProvisioningMode = autopilotProvisioningMode, SelectedAutopilotProfile = selectedAutopilotProfile, AutopilotHardwareHashUpload = autopilotHardwareHashUpload ?? new DeployAutopilotHardwareHashUploadSettings(), + OsRecovery = osRecovery ?? new DeployOsRecoverySettings(), Oobe = oobe ?? new DeployOobeSettings(), AppxRemoval = appxRemoval ?? new DeployAppxRemovalSettings(), AiComponentRemoval = aiComponentRemoval ?? new DeployAiComponentRemovalSettings(), diff --git a/src/Foundry.Deploy.Tests/DeploymentOrchestratorTests.cs b/src/Foundry.Deploy.Tests/DeploymentOrchestratorTests.cs index a1ec7ac0..68b295b8 100644 --- a/src/Foundry.Deploy.Tests/DeploymentOrchestratorTests.cs +++ b/src/Foundry.Deploy.Tests/DeploymentOrchestratorTests.cs @@ -12,13 +12,19 @@ namespace Foundry.Deploy.Tests; public sealed class DeploymentOrchestratorTests { [Fact] - public void DeploymentStepNames_All_OrdersAutopilotProvisioningAfterRecoverySeal() + public void DeploymentStepNames_All_OrdersOsRecoveryAfterDriverPackAndBeforeFirmware() { List steps = DeploymentStepNames.All.ToList(); + int driverPackIndex = steps.IndexOf(DeploymentStepNames.ApplyDriverPack); + int osRecoveryIndex = steps.IndexOf(DeploymentStepNames.ProvisionOsRecovery); + int firmwareIndex = steps.IndexOf(DeploymentStepNames.DownloadFirmwareUpdate); int sealIndex = steps.IndexOf(DeploymentStepNames.SealRecoveryPartition); int autopilotIndex = steps.IndexOf(DeploymentStepNames.ProvisionAutopilot); int finalizeIndex = steps.IndexOf(DeploymentStepNames.FinalizeDeploymentAndWriteLogs); + Assert.True(driverPackIndex >= 0); + Assert.Equal(driverPackIndex + 1, osRecoveryIndex); + Assert.Equal(osRecoveryIndex + 1, firmwareIndex); Assert.True(sealIndex >= 0); Assert.Equal(sealIndex + 1, autopilotIndex); Assert.Equal(autopilotIndex + 1, finalizeIndex); diff --git a/src/Foundry.Deploy.Tests/DeploymentRuntimeContextServiceTests.cs b/src/Foundry.Deploy.Tests/DeploymentRuntimeContextServiceTests.cs new file mode 100644 index 00000000..4c13efe2 --- /dev/null +++ b/src/Foundry.Deploy.Tests/DeploymentRuntimeContextServiceTests.cs @@ -0,0 +1,27 @@ +using Foundry.Deploy.Models; +using Foundry.Deploy.Services.Runtime; + +namespace Foundry.Deploy.Tests; + +public sealed class DeploymentRuntimeContextServiceTests : IDisposable +{ + private const string DeploymentModeEnvironmentVariable = "FOUNDRY_DEPLOYMENT_MODE"; + private readonly string? _originalMode = Environment.GetEnvironmentVariable(DeploymentModeEnvironmentVariable); + + [Fact] + public void Resolve_WhenEnvironmentModeIsRecovery_ReturnsRecoveryContext() + { + Environment.SetEnvironmentVariable(DeploymentModeEnvironmentVariable, "Recovery"); + var service = new DeploymentRuntimeContextService(); + + DeploymentRuntimeContext context = service.Resolve(); + + Assert.Equal(DeploymentMode.Recovery, context.Mode); + Assert.Null(context.UsbCacheRuntimeRoot); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(DeploymentModeEnvironmentVariable, _originalMode); + } +} diff --git a/src/Foundry.Deploy.Tests/DeploymentStepExecutionContextTests.cs b/src/Foundry.Deploy.Tests/DeploymentStepExecutionContextTests.cs index 2aff1383..4bae9f55 100644 --- a/src/Foundry.Deploy.Tests/DeploymentStepExecutionContextTests.cs +++ b/src/Foundry.Deploy.Tests/DeploymentStepExecutionContextTests.cs @@ -107,6 +107,22 @@ public void ResolveDriverPackCacheRoot_WhenIsoTargetRootIsResolved_UsesTargetCac Assert.Equal(Path.Combine(targetFoundryRoot, "Cache", "DriverPacks"), root); } + [Fact] + public void ResolveOperatingSystemCacheRoot_WhenRecoveryTargetRootIsResolved_UsesTargetCacheLayout() + { + using TempDeploymentWorkspace workspace = TempDeploymentWorkspace.Create(); + string targetFoundryRoot = Path.Combine(workspace.RootPath, "Windows", "Foundry"); + DeploymentStepExecutionContext context = CreateExecutionContext( + workspace.RootPath, + resolvedCacheRootPath: Path.Combine(workspace.CacheRootPath, "Runtime"), + mode: DeploymentMode.Recovery, + targetFoundryRoot: targetFoundryRoot); + + string root = context.ResolveOperatingSystemCacheRoot(); + + Assert.Equal(Path.Combine(targetFoundryRoot, "Cache", "OperatingSystems"), root); + } + private static DeploymentStepExecutionContext CreateExecutionContext( string workspaceRoot, string resolvedCacheRootPath, diff --git a/src/Foundry.Deploy.Tests/HardwareProfileServiceTests.cs b/src/Foundry.Deploy.Tests/HardwareProfileServiceTests.cs new file mode 100644 index 00000000..0b20bd0c --- /dev/null +++ b/src/Foundry.Deploy.Tests/HardwareProfileServiceTests.cs @@ -0,0 +1,105 @@ +using Foundry.Deploy.Models; +using Foundry.Deploy.Services.Hardware; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Foundry.Deploy.Tests; + +public sealed class HardwareProfileServiceTests +{ + [Fact] + public async Task GetCurrentAsync_WhenNativeSourceReturnsHardwareData_MapsProfile() + { + var source = new FakeHardwareProfileSource + { + Snapshot = new HardwareProfileSnapshot( + Manufacturer: " Dell Inc. ", + Model: " Latitude 7450 ", + Product: " 1.0 ", + SerialNumber: " ABC123 ", + IsOnBattery: true, + Devices: + [ + new PnpDeviceInfo + { + Name = "System Firmware", + DeviceId = @"UEFI\RES_{9f41a8c2-5f6c-4f1d-9b8c-7a6e5d4c3b2a}", + ClassGuid = "{f2e7dd72-6468-4e36-b6f1-6488f42c1b52}", + HardwareIds = [@"UEFI\RES_{9f41a8c2-5f6c-4f1d-9b8c-7a6e5d4c3b2a}"] + }, + new PnpDeviceInfo + { + Name = "Trusted Platform Module 2.0", + DeviceId = @"ACPI\MSFT0101\1", + PnpClass = "SecurityDevices", + HardwareIds = [@"ACPI\MSFT0101"] + } + ]) + }; + var service = new HardwareProfileService( + source, + NullLogger.Instance, + () => "AMD64"); + + HardwareProfile profile = await service.GetCurrentAsync(TestContext.Current.CancellationToken); + + Assert.Equal("Dell", profile.Manufacturer); + Assert.Equal("Latitude 7450", profile.Model); + Assert.Equal("1.0", profile.Product); + Assert.Equal("ABC123", profile.SerialNumber); + Assert.Equal("x64", profile.Architecture); + Assert.True(profile.IsOnBattery); + Assert.True(profile.IsTpmPresent); + Assert.Equal("9f41a8c2-5f6c-4f1d-9b8c-7a6e5d4c3b2a", profile.SystemFirmwareHardwareId); + Assert.Equal(2, profile.PnpDevices.Count); + Assert.Equal(1, source.Calls); + } + + [Fact] + public async Task GetCurrentAsync_WhenNativeSourceFails_ReturnsFallbackProfile() + { + var source = new FakeHardwareProfileSource + { + Exception = new InvalidOperationException("SetupAPI unavailable") + }; + var service = new HardwareProfileService( + source, + NullLogger.Instance, + () => string.Empty); + + HardwareProfile profile = await service.GetCurrentAsync(TestContext.Current.CancellationToken); + + Assert.Equal("Unknown", profile.Manufacturer); + Assert.Equal("Unknown", profile.Model); + Assert.Equal("Unknown", profile.Product); + Assert.Equal("Unknown", profile.SerialNumber); + Assert.False(profile.IsOnBattery); + Assert.False(profile.IsTpmPresent); + Assert.Empty(profile.SystemFirmwareHardwareId); + Assert.Empty(profile.PnpDevices); + } + + private sealed class FakeHardwareProfileSource : IHardwareProfileSource + { + public HardwareProfileSnapshot Snapshot { get; init; } = new( + Manufacturer: string.Empty, + Model: string.Empty, + Product: string.Empty, + SerialNumber: string.Empty, + IsOnBattery: false, + Devices: []); + + public Exception? Exception { get; init; } + public int Calls { get; private set; } + + public HardwareProfileSnapshot Capture() + { + Calls++; + if (Exception is not null) + { + throw Exception; + } + + return Snapshot; + } + } +} diff --git a/src/Foundry.Deploy.Tests/PreOobeNetworkProfileRoamingStepTests.cs b/src/Foundry.Deploy.Tests/PreOobeNetworkProfileRoamingStepTests.cs index 62a83612..ae9f86e4 100644 --- a/src/Foundry.Deploy.Tests/PreOobeNetworkProfileRoamingStepTests.cs +++ b/src/Foundry.Deploy.Tests/PreOobeNetworkProfileRoamingStepTests.cs @@ -166,6 +166,19 @@ public DriverPackExecutionPlan Resolve( private sealed class FakeWindowsDeploymentService : IWindowsDeploymentService { public Task PrepareTargetDiskAsync(int diskNumber, string workingDirectory, CancellationToken cancellationToken = default) + { + return PrepareTargetDiskAsync( + diskNumber, + workingDirectory, + RecoveryTargetDiskLayoutMode.FullWipe, + cancellationToken); + } + + public Task PrepareTargetDiskAsync( + int diskNumber, + string workingDirectory, + RecoveryTargetDiskLayoutMode layoutMode, + CancellationToken cancellationToken = default) { throw new NotSupportedException(); } diff --git a/src/Foundry.Deploy.Tests/PrepareTargetDiskLayoutStepTests.cs b/src/Foundry.Deploy.Tests/PrepareTargetDiskLayoutStepTests.cs index 59408b5f..b960b58c 100644 --- a/src/Foundry.Deploy.Tests/PrepareTargetDiskLayoutStepTests.cs +++ b/src/Foundry.Deploy.Tests/PrepareTargetDiskLayoutStepTests.cs @@ -78,6 +78,19 @@ public Task PrepareTargetDiskAsync( int diskNumber, string workingDirectory, CancellationToken cancellationToken = default) + { + return PrepareTargetDiskAsync( + diskNumber, + workingDirectory, + RecoveryTargetDiskLayoutMode.FullWipe, + cancellationToken); + } + + public Task PrepareTargetDiskAsync( + int diskNumber, + string workingDirectory, + RecoveryTargetDiskLayoutMode layoutMode, + CancellationToken cancellationToken = default) { Directory.CreateDirectory(workingDirectory); diff --git a/src/Foundry.Deploy.Tests/ProvisionOsRecoveryStepTests.cs b/src/Foundry.Deploy.Tests/ProvisionOsRecoveryStepTests.cs new file mode 100644 index 00000000..f0e87f90 --- /dev/null +++ b/src/Foundry.Deploy.Tests/ProvisionOsRecoveryStepTests.cs @@ -0,0 +1,438 @@ +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; +using Foundry.Core.Services.WinPe; +using Foundry.Core.Services.WinPe.OsRecovery; +using Foundry.Deploy.Models; +using Foundry.Deploy.Models.Configuration; +using Foundry.Deploy.Services.Configuration; +using Foundry.Deploy.Services.Deployment; +using Foundry.Deploy.Services.Deployment.Steps; +using Foundry.Deploy.Services.Hardware; +using Foundry.Deploy.Services.Logging; +using Foundry.Deploy.Services.Operations; +using Foundry.Deploy.Services.System; +using CoreDeployNetworkSettings = Foundry.Core.Models.Configuration.Deploy.DeployNetworkSettings; +using CoreDeployNetworkProfileRoamingSettings = Foundry.Core.Models.Configuration.Deploy.DeployNetworkProfileRoamingSettings; + +namespace Foundry.Deploy.Tests; + +public sealed class ProvisionOsRecoveryStepTests +{ + [Fact] + public async Task ExecuteAsync_WhenOsRecoveryIsDisabled_SkipsWithoutProvisioning() + { + using TempWorkspace workspace = TempWorkspace.Create(); + var provisioning = new RecordingOsRecoveryPayloadProvisioningService(); + ProvisionOsRecoveryStep step = CreateStep(provisioning); + DeploymentStepExecutionContext context = CreateContext(workspace.RootPath, isOsRecoveryEnabled: false); + + DeploymentStepResult result = await step.ExecuteAsync(context, TestContext.Current.CancellationToken); + + Assert.Equal(DeploymentStepState.Skipped, result.State); + Assert.Equal("OS Recovery is disabled.", result.Message); + Assert.Equal(0, provisioning.CallCount); + } + + [Fact] + public async Task ExecuteAsync_WhenRunningFromRecoveryMode_SkipsWithoutProvisioning() + { + using TempWorkspace workspace = TempWorkspace.Create(); + var provisioning = new RecordingOsRecoveryPayloadProvisioningService(); + ProvisionOsRecoveryStep step = CreateStep(provisioning); + DeploymentStepExecutionContext context = CreateContext( + workspace.RootPath, + isOsRecoveryEnabled: true, + mode: DeploymentMode.Recovery); + + DeploymentStepResult result = await step.ExecuteAsync(context, TestContext.Current.CancellationToken); + + Assert.Equal(DeploymentStepState.Skipped, result.State); + Assert.Equal("OS Recovery provisioning is skipped in recovery mode.", result.Message); + Assert.Equal(0, provisioning.CallCount); + } + + [Fact] + public async Task ExecuteAsync_WhenRecoveryPartitionIsUnavailable_FailsBeforeProvisioning() + { + using TempWorkspace workspace = TempWorkspace.Create(); + var provisioning = new RecordingOsRecoveryPayloadProvisioningService(); + ProvisionOsRecoveryStep step = CreateStep(provisioning); + DeploymentStepExecutionContext context = CreateContext( + workspace.RootPath, + isOsRecoveryEnabled: true, + mode: DeploymentMode.Iso); + + DeploymentStepResult result = await step.ExecuteAsync(context, TestContext.Current.CancellationToken); + + Assert.Equal(DeploymentStepState.Failed, result.State); + Assert.Equal("Recovery partition is unavailable.", result.Message); + Assert.Equal(0, provisioning.CallCount); + } + + [Fact] + public async Task ExecuteAsync_WhenDryRunAndEnabled_SucceedsWithoutProvisioning() + { + using TempWorkspace workspace = TempWorkspace.Create(); + var provisioning = new RecordingOsRecoveryPayloadProvisioningService(); + ProvisionOsRecoveryStep step = CreateStep(provisioning); + DeploymentStepExecutionContext context = CreateContext( + workspace.RootPath, + isOsRecoveryEnabled: true, + isDryRun: true); + + DeploymentStepResult result = await step.ExecuteAsync(context, TestContext.Current.CancellationToken); + + Assert.Equal(DeploymentStepState.Succeeded, result.State); + Assert.Equal("OS Recovery provisioned (simulation).", result.Message); + Assert.Equal(0, provisioning.CallCount); + } + + [Fact] + public async Task ExecuteAsync_WhenLiveAndEnabled_ProvisionsSanitizedRecoveryPayload() + { + using TempWorkspace workspace = TempWorkspace.Create(); + string windowsRoot = Path.Combine(workspace.RootPath, "W"); + string recoveryRoot = Path.Combine(workspace.RootPath, "R"); + Directory.CreateDirectory(Path.Combine(windowsRoot, "Windows")); + Directory.CreateDirectory(Path.Combine(recoveryRoot, "Recovery", "WindowsRE")); + await File.WriteAllTextAsync( + Path.Combine(recoveryRoot, "Recovery", "WindowsRE", "winre.wim"), + "wim", + TestContext.Current.CancellationToken); + + string deployConfigurationPath = Path.Combine(workspace.RootPath, "foundry.deploy.config.json"); + FoundryDeployConfigurationDocument sourceDocument = new() + { + OsRecovery = new DeployOsRecoverySettings + { + IsEnabled = true + }, + Autopilot = new DeployAutopilotSettings + { + IsEnabled = true, + DefaultProfileFolderName = "profile" + }, + Network = new CoreDeployNetworkSettings + { + ProfileRoaming = new CoreDeployNetworkProfileRoamingSettings + { + IsEnabled = true, + IncludePrivateKeyMaterial = true, + ArtifactRootPath = @"X:\Foundry\Config\Network" + } + } + }; + await File.WriteAllTextAsync( + deployConfigurationPath, + System.Text.Json.JsonSerializer.Serialize(sourceDocument, ConfigurationJsonDefaults.SerializerOptions), + TestContext.Current.CancellationToken); + + var provisioning = new RecordingOsRecoveryPayloadProvisioningService(); + var processRunner = new RecordingProcessRunner(); + ProvisionOsRecoveryStep step = new( + provisioning, + new FakeEmbeddedAssetService(), + new FakeDeployConfigurationService(deployConfigurationPath), + processRunner, + winReConfigToolPath: Path.Combine(workspace.RootPath, "winrecfg.exe")); + DeploymentStepExecutionContext context = CreateContext( + workspace.RootPath, + isOsRecoveryEnabled: true, + mode: DeploymentMode.Iso, + isDryRun: false, + runtimeState => + { + runtimeState.TargetWindowsPartitionRoot = windowsRoot; + runtimeState.TargetRecoveryPartitionRoot = recoveryRoot; + runtimeState.TargetFoundryRoot = Path.Combine(windowsRoot, "Foundry"); + runtimeState.WinReConfigured = true; + Directory.CreateDirectory(runtimeState.TargetFoundryRoot); + }); + + DeploymentStepResult result = await step.ExecuteAsync(context, TestContext.Current.CancellationToken); + + Assert.Equal(DeploymentStepState.Succeeded, result.State); + Assert.Equal(1, provisioning.CallCount); + Assert.Contains(processRunner.Calls, call => call.Contains("/Mount-Image", StringComparison.Ordinal)); + Assert.Contains(processRunner.Calls, call => call.Contains("/Unmount-Image", StringComparison.Ordinal) && call.Contains("/Commit", StringComparison.Ordinal)); + Assert.Contains(processRunner.Calls, call => call.Contains("/setbootshelllink", StringComparison.Ordinal)); + Assert.True(File.Exists(Path.Combine(recoveryRoot, "Recovery", "WindowsRE", "FoundryOsRecovery.json"))); + + FoundryDeployConfigurationDocument recoveryDeployDocument = + System.Text.Json.JsonSerializer.Deserialize( + provisioning.CapturedOptions!.DeployConfigurationJson, + ConfigurationJsonDefaults.SerializerOptions)!; + FoundryConnectConfigurationDocument recoveryConnectDocument = + System.Text.Json.JsonSerializer.Deserialize( + provisioning.CapturedOptions.FoundryConnectConfigurationJson, + ConfigurationJsonDefaults.SerializerOptions)!; + + Assert.False(recoveryDeployDocument.Autopilot.IsEnabled); + Assert.False(recoveryDeployDocument.Network.ProfileRoaming.IsEnabled); + Assert.False(recoveryConnectDocument.Wifi.IsEnabled); + Assert.False(recoveryConnectDocument.Dot1x.IsEnabled); + Assert.Equal(Path.Combine(Environment.SystemDirectory, "curl.exe"), provisioning.CapturedOptions.CurlExecutableSourcePath); + } + + private static ProvisionOsRecoveryStep CreateStep(RecordingOsRecoveryPayloadProvisioningService provisioning) + { + return new ProvisionOsRecoveryStep( + provisioning, + new FakeEmbeddedAssetService(), + new FakeDeployConfigurationService(), + new NoOpProcessRunner()); + } + + private static DeploymentStepExecutionContext CreateContext( + string workspaceRoot, + bool isOsRecoveryEnabled, + DeploymentMode mode = DeploymentMode.Iso, + bool isDryRun = false, + Action? configureRuntimeState = null) + { + DeploymentContext request = new() + { + Mode = mode, + CacheRootPath = workspaceRoot, + TargetDiskNumber = 1, + TargetComputerName = "LAB01", + OperatingSystem = new OperatingSystemCatalogItem + { + Architecture = "x64" + }, + DriverPackSelectionKind = DriverPackSelectionKind.None, + OsRecovery = new DeployOsRecoverySettings + { + IsEnabled = isOsRecoveryEnabled + }, + IsDryRun = isDryRun + }; + + DeploymentRuntimeState runtimeState = new() + { + WorkspaceRoot = workspaceRoot, + Mode = mode, + TargetDiskNumber = 1, + IsOsRecoveryEnabled = isOsRecoveryEnabled, + IsDryRun = isDryRun + }; + configureRuntimeState?.Invoke(runtimeState); + + return new DeploymentStepExecutionContext( + request, + runtimeState, + [DeploymentStepNames.ProvisionOsRecovery], + new FakeOperationProgressService(), + new FakeDeploymentLogService(), + new FakeTargetDiskService(), + _ => { }); + } + + private sealed class RecordingOsRecoveryPayloadProvisioningService : IOsRecoveryPayloadProvisioningService + { + public int CallCount { get; private set; } + public OsRecoveryPayloadProvisioningOptions? CapturedOptions { get; private set; } + + public Task> ProvisionAsync( + OsRecoveryPayloadProvisioningOptions options, + IProgress? downloadProgress = null, + CancellationToken cancellationToken = default) + { + CallCount++; + CapturedOptions = options; + return Task.FromResult(WinPeResult.Success(new OsRecoveryPayloadProvisioningResult + { + BootMenuConfigurationXml = "", + ManagedPayloadSizeBytes = 1 + })); + } + } + + private sealed class FakeEmbeddedAssetService : IWinPeEmbeddedAssetService + { + public string GetBootstrapScriptContent() => "bootstrap"; + public string GetUsbProvisioningScriptTemplateContent() => "usb"; + public string GetIanaWindowsTimeZoneMapJson() => "{}"; + public string GetSevenZipSourceDirectoryPath() => string.Empty; + } + + private sealed class FakeDeployConfigurationService : IDeployConfigurationService + { + private readonly string? _configurationPath; + + public FakeDeployConfigurationService(string? configurationPath = null) + { + _configurationPath = configurationPath; + } + + public DeployConfigurationLoadResult LoadOptional() + { + return string.IsNullOrWhiteSpace(_configurationPath) + ? new DeployConfigurationLoadResult() + : new DeployConfigurationLoadResult + { + Exists = true, + ConfigurationPath = _configurationPath + }; + } + } + + private sealed class NoOpProcessRunner : IProcessRunner + { + public Task RunAsync( + string fileName, + string arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 }); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 }); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + Action? onOutputData, + Action? onErrorData, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 }); + } + } + + private sealed class RecordingProcessRunner : IProcessRunner + { + public List Calls { get; } = []; + + public Task RunAsync( + string fileName, + string arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + Calls.Add($"{fileName} {arguments}"); + return Task.FromResult(new ProcessExecutionResult { ExitCode = 0, FileName = fileName, Arguments = arguments }); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + string joinedArguments = string.Join(' ', arguments); + Calls.Add($"{fileName} {joinedArguments}"); + return Task.FromResult(new ProcessExecutionResult { ExitCode = 0, FileName = fileName, Arguments = joinedArguments }); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + Action? onOutputData, + Action? onErrorData, + CancellationToken cancellationToken = default) + { + return RunAsync(fileName, arguments, workingDirectory, cancellationToken); + } + } + + private sealed class FakeOperationProgressService : IOperationProgressService + { + public bool IsOperationInProgress => false; + public int Progress => 0; + public string? Status => null; + public OperationKind? CurrentOperation => null; + public bool CanStartOperation => true; + public event EventHandler? ProgressChanged; + public bool TryStart(OperationKind kind, string initialStatus, int initialProgress = 0) => true; + public void Report(int progress, string? status = null) => ProgressChanged?.Invoke(this, EventArgs.Empty); + public void Complete(string? status = null) => ProgressChanged?.Invoke(this, EventArgs.Empty); + public void Fail(string status) => ProgressChanged?.Invoke(this, EventArgs.Empty); + public void ResetToIdle() => ProgressChanged?.Invoke(this, EventArgs.Empty); + } + + private sealed class FakeDeploymentLogService : IDeploymentLogService + { + public DeploymentLogSession Initialize(string rootPath) + { + string logsDirectory = Path.Combine(rootPath, "Logs"); + string stateDirectory = Path.Combine(rootPath, "State"); + Directory.CreateDirectory(logsDirectory); + Directory.CreateDirectory(stateDirectory); + return new DeploymentLogSession + { + RootPath = rootPath, + LogsDirectoryPath = logsDirectory, + StateDirectoryPath = stateDirectory, + LogFilePath = Path.Combine(logsDirectory, "FoundryDeploy.log"), + StateFilePath = Path.Combine(stateDirectory, "deployment-state.json") + }; + } + + public Task AppendAsync( + DeploymentLogSession session, + DeploymentLogLevel level, + string message, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task SaveStateAsync( + DeploymentLogSession session, + TState state, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Release(DeploymentLogSession session) + { + } + } + + private sealed class FakeTargetDiskService : ITargetDiskService + { + public Task> GetDisksAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult>([]); + } + + public Task GetDiskNumberForPathAsync(string path, CancellationToken cancellationToken = default) + { + return Task.FromResult(null); + } + } + + private sealed class TempWorkspace : IDisposable + { + private TempWorkspace(string rootPath) + { + RootPath = rootPath; + } + + public string RootPath { get; } + + public static TempWorkspace Create() + { + string rootPath = Path.Combine(Path.GetTempPath(), $"foundry-osrecovery-step-{Guid.NewGuid():N}"); + Directory.CreateDirectory(rootPath); + return new TempWorkspace(rootPath); + } + + public void Dispose() + { + Directory.Delete(RootPath, recursive: true); + } + } +} diff --git a/src/Foundry.Deploy.Tests/RecoveryTargetDiskResolverTests.cs b/src/Foundry.Deploy.Tests/RecoveryTargetDiskResolverTests.cs new file mode 100644 index 00000000..d9c75349 --- /dev/null +++ b/src/Foundry.Deploy.Tests/RecoveryTargetDiskResolverTests.cs @@ -0,0 +1,157 @@ +using Foundry.Deploy.Services.Runtime; +using Foundry.Deploy.Services.System; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Foundry.Deploy.Tests; + +public sealed class RecoveryTargetDiskResolverTests +{ + [Fact] + public async Task ResolveAsync_WhenExactlyOneFoundryRecoveryMarkerExists_ReturnsDiskNumber() + { + var processRunner = new DiskPartProcessRunner(existingLetter: null); + var resolver = new RecoveryTargetDiskResolver( + processRunner, + NullLogger.Instance, + path => path.EndsWith(@"Recovery\WindowsRE\FoundryOsRecovery.json", StringComparison.OrdinalIgnoreCase)); + + int? diskNumber = await resolver.ResolveAsync(TestContext.Current.CancellationToken); + + Assert.Equal(2, diskNumber); + Assert.Contains(processRunner.Calls, call => call.StartsWith("diskpart.exe ", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(processRunner.Calls, call => call.StartsWith("powershell.exe ", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task ResolveAsync_WhenFoundryRecoveryMarkerIsMissing_ReturnsNull() + { + var processRunner = new DiskPartProcessRunner(existingLetter: null); + var resolver = new RecoveryTargetDiskResolver( + processRunner, + NullLogger.Instance, + _ => false); + + int? diskNumber = await resolver.ResolveAsync(TestContext.Current.CancellationToken); + + Assert.Null(diskNumber); + Assert.DoesNotContain(processRunner.Calls, call => call.StartsWith("powershell.exe ", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task ResolveAsync_WhenRecoveryPartitionAlreadyHasDriveLetter_UsesExistingLetter() + { + var processRunner = new DiskPartProcessRunner(existingLetter: 'R'); + var resolver = new RecoveryTargetDiskResolver( + processRunner, + NullLogger.Instance, + path => path.Equals(@"R:\Recovery\WindowsRE\FoundryOsRecovery.json", StringComparison.OrdinalIgnoreCase)); + + int? diskNumber = await resolver.ResolveAsync(TestContext.Current.CancellationToken); + + Assert.Equal(2, diskNumber); + Assert.DoesNotContain(processRunner.ScriptContents, script => script.Contains("assign letter=", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(processRunner.Calls, call => call.StartsWith("powershell.exe ", StringComparison.OrdinalIgnoreCase)); + } + + private sealed class DiskPartProcessRunner(char? existingLetter) : IProcessRunner + { + public List Calls { get; } = []; + public List ScriptContents { get; } = []; + + public Task RunAsync( + string fileName, + string arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + Calls.Add($"{fileName} {arguments}"); + return Task.FromResult(new ProcessExecutionResult + { + ExitCode = 0, + FileName = fileName, + Arguments = arguments, + WorkingDirectory = workingDirectory, + StandardOutput = CreateOutput(fileName, arguments, existingLetter) + }); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + return RunAsync(fileName, string.Join(' ', arguments), workingDirectory, cancellationToken); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + Action? onOutputData, + Action? onErrorData, + CancellationToken cancellationToken = default) + { + return RunAsync(fileName, arguments, workingDirectory, cancellationToken); + } + + private string CreateOutput(string fileName, string arguments, char? existingLetter) + { + if (!string.Equals(fileName, "diskpart.exe", StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + string script = File.ReadAllText(arguments.Replace("/s ", string.Empty, StringComparison.Ordinal).Trim('"')); + ScriptContents.Add(script); + if (script.Contains("detail partition", StringComparison.OrdinalIgnoreCase)) + { + if (!script.Contains("select partition 3", StringComparison.OrdinalIgnoreCase)) + { + string typeGuid = script.Contains("select partition 1", StringComparison.OrdinalIgnoreCase) + ? "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" + : "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"; + + return $$""" + Partition 1 + Type : {{typeGuid}} + """; + } + + string volumeLine = existingLetter.HasValue + ? $" Volume 3 {existingLetter.Value} Recovery NTFS Partition 5120 MB Healthy Hidden" + : " Volume 3 Recovery NTFS Partition 5120 MB Healthy Hidden"; + + return $$""" + Partition 3 + Type : de94bba4-06d1-4d40-a16a-bfd50179d6ac + Hidden: Yes + Required: Yes + Attrib: 0X8000000000000001 + + Volume ### Ltr Label Fs Type Size Status Info + ---------- --- ----------- ----- ---------- ------- --------- -------- + {{volumeLine}} + """; + } + + if (script.Contains("list partition", StringComparison.OrdinalIgnoreCase)) + { + return """ + Partition ### Type Size Offset + ------------- ---------------- ------- ------- + Partition 1 System 260 MB 1024 KB + Partition 2 Reserved 16 MB 261 MB + Partition 3 Recovery 5120 MB 277 MB + Partition 4 Primary 470 GB 5397 MB + """; + } + + return """ + Disk ### Status Size Free Dyn Gpt + -------- ------------- ------- ------- --- --- + Disk 2 Online 476 GB 0 B * + """; + } + } +} diff --git a/src/Foundry.Deploy.Tests/TargetDiskServiceTests.cs b/src/Foundry.Deploy.Tests/TargetDiskServiceTests.cs new file mode 100644 index 00000000..3e9a3610 --- /dev/null +++ b/src/Foundry.Deploy.Tests/TargetDiskServiceTests.cs @@ -0,0 +1,284 @@ +using Foundry.Deploy.Models; +using Foundry.Deploy.Services.Hardware; +using Foundry.Deploy.Services.System; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Foundry.Deploy.Tests; + +public sealed class TargetDiskServiceTests +{ + [Fact] + public async Task GetDisksAsync_UsesDiskPartAndParsesDiskInventory() + { + var processRunner = new DiskPartProcessRunner(localizedOutput: false); + var service = new TargetDiskService(processRunner, NullLogger.Instance); + + IReadOnlyList disks = await service.GetDisksAsync(TestContext.Current.CancellationToken); + + TargetDiskInfo disk = Assert.Single(disks); + Assert.Equal(0, disk.DiskNumber); + Assert.Equal("NVMe Foundry Disk", disk.FriendlyName); + Assert.Equal("NVME123", disk.SerialNumber); + Assert.Equal("NVMe", disk.BusType); + Assert.Equal("GPT", disk.PartitionStyle); + Assert.Equal(512UL * 1024UL * 1024UL * 1024UL, disk.SizeBytes); + Assert.True(disk.IsSelectable); + Assert.Contains(processRunner.Calls, call => call.StartsWith("diskpart.exe ", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(processRunner.Calls, call => call.StartsWith("powershell.exe ", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task GetDiskNumberForPathAsync_UsesDiskPartDetailVolume() + { + var processRunner = new DiskPartProcessRunner(localizedOutput: false); + var service = new TargetDiskService(processRunner, NullLogger.Instance); + + int? diskNumber = await service.GetDiskNumberForPathAsync(@"W:\Windows", TestContext.Current.CancellationToken); + + Assert.Equal(0, diskNumber); + Assert.Contains(processRunner.Calls, call => call.StartsWith("diskpart.exe ", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(processRunner.Calls, call => call.StartsWith("powershell.exe ", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task GetDisksAsync_WhenDiskPartOutputIsLocalized_ParsesDiskInventory() + { + var processRunner = new DiskPartProcessRunner(localizedOutput: true); + var service = new TargetDiskService(processRunner, NullLogger.Instance); + + IReadOnlyList disks = await service.GetDisksAsync(TestContext.Current.CancellationToken); + + TargetDiskInfo disk = Assert.Single(disks); + Assert.Equal(0, disk.DiskNumber); + Assert.Equal("Disque Foundry NVMe", disk.FriendlyName); + Assert.Equal("NVME123", disk.SerialNumber); + Assert.Equal(512UL * 1024UL * 1024UL * 1024UL, disk.SizeBytes); + Assert.True(disk.IsSelectable); + } + + [Fact] + public async Task GetDisksAsync_WhenDiskPartDetailIncludesBanner_UsesDiskModelAsFriendlyName() + { + var processRunner = new DiskPartProcessRunner(localizedOutput: false, includeBanner: true); + var service = new TargetDiskService(processRunner, NullLogger.Instance); + + IReadOnlyList disks = await service.GetDisksAsync(TestContext.Current.CancellationToken); + + TargetDiskInfo disk = Assert.Single(disks); + Assert.Equal("NVMe Foundry Disk", disk.FriendlyName); + } + + [Fact] + public async Task GetDisksAsync_WhenDiskPartSelectionTextIsUnknownLanguage_UsesDiskModelAsFriendlyName() + { + var processRunner = new DiskPartProcessRunner(localizedOutput: false, includeUnknownLanguageSelectionText: true); + var service = new TargetDiskService(processRunner, NullLogger.Instance); + + IReadOnlyList disks = await service.GetDisksAsync(TestContext.Current.CancellationToken); + + TargetDiskInfo disk = Assert.Single(disks); + Assert.Equal("NVMe Foundry Disk", disk.FriendlyName); + } + + [Fact] + public async Task GetDisksAsync_WhenDiskPartTypeKeyIsUnavailable_InfersBusTypeFromHardwareTokens() + { + var processRunner = new DiskPartProcessRunner(localizedOutput: false, omitTypeKey: true); + var service = new TargetDiskService(processRunner, NullLogger.Instance); + + IReadOnlyList disks = await service.GetDisksAsync(TestContext.Current.CancellationToken); + + TargetDiskInfo disk = Assert.Single(disks); + Assert.Equal("NVMe", disk.BusType); + } + + private sealed class DiskPartProcessRunner( + bool localizedOutput, + bool includeBanner = false, + bool includeUnknownLanguageSelectionText = false, + bool omitTypeKey = false) : IProcessRunner + { + private readonly bool _localizedOutput = localizedOutput; + private readonly bool _includeBanner = includeBanner; + private readonly bool _includeUnknownLanguageSelectionText = includeUnknownLanguageSelectionText; + private readonly bool _omitTypeKey = omitTypeKey; + + public List Calls { get; } = []; + + public Task RunAsync( + string fileName, + string arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + Calls.Add($"{fileName} {arguments}"); + return Task.FromResult(CreateResult(fileName, arguments)); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + CancellationToken cancellationToken = default) + { + return RunAsync(fileName, string.Join(' ', arguments), workingDirectory, cancellationToken); + } + + public Task RunAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + Action? onOutputData, + Action? onErrorData, + CancellationToken cancellationToken = default) + { + return RunAsync(fileName, arguments, workingDirectory, cancellationToken); + } + + private ProcessExecutionResult CreateResult(string fileName, string arguments) + { + if (!string.Equals(fileName, "diskpart.exe", StringComparison.OrdinalIgnoreCase)) + { + return new ProcessExecutionResult { ExitCode = 0 }; + } + + string script = File.ReadAllText(arguments.Replace("/s ", string.Empty, StringComparison.Ordinal).Trim('"')); + string output = script.Contains("detail disk", StringComparison.OrdinalIgnoreCase) + ? CreateDetailDiskOutput(script, _localizedOutput, _includeBanner, _includeUnknownLanguageSelectionText, _omitTypeKey) + : script.Contains("detail volume", StringComparison.OrdinalIgnoreCase) + ? """ + Disk ### Status Size Free Dyn Gpt + -------- ------------- ------- ------- --- --- + Disk 0 Online 512 GB 0 B * + """ + : _localizedOutput + ? """ + N° disque Statut Taille Libre Dyn GPT + --------- ------------- ------- ------- --- --- + Disque 0 En ligne 512 G octets 0 octets * + Disque 1 En ligne 32 G octets 0 octets + """ + : """ + Disk ### Status Size Free Dyn Gpt + -------- ------------- ------- ------- --- --- + Disk 0 Online 512 GB 0 B * + Disk 1 Online 32 GB 0 B + """; + + return new ProcessExecutionResult + { + ExitCode = 0, + FileName = fileName, + Arguments = arguments, + StandardOutput = output + }; + } + + private static string CreateDetailDiskOutput( + string script, + bool localizedOutput, + bool includeBanner, + bool includeUnknownLanguageSelectionText, + bool omitTypeKey) + { + if (script.Contains("select disk 1", StringComparison.OrdinalIgnoreCase)) + { + string usbOutput = localizedOutput + ? """ + Disque USB Foundry + Type : USB + Statut : En ligne + État de lecture seule actuel : Non + Lecture seule : Non + Disque de démarrage : Non + Numéro de série : USB123 + """ + : """ + USB Foundry Disk + Type : USB + Status : Online + Current Read-only State : No + Read-only : No + Boot Disk : No + Serial Number : USB123 + """; + + return WrapDetailOutput(usbOutput, includeBanner, includeUnknownLanguageSelectionText); + } + + string output = localizedOutput + ? """ + Disque Foundry NVMe + Type : NVMe + Statut : En ligne + État de lecture seule actuel : Non + Lecture seule : Non + Disque de démarrage : Non + Disque système : Non + Numéro de série : NVME123 + """ + : """ + NVMe Foundry Disk + Disk ID: {00000000-0000-0000-0000-000000000000} + Type : NVMe + Status : Online + Path : 0 + Target : 0 + LUN ID : 0 + Location Path : PCIROOT(0)#PCI(0100)#NVME(P00T00L00) + Current Read-only State : No + Read-only : No + Boot Disk : No + Pagefile Disk : No + Hibernation File Disk : No + Crashdump Disk : No + Clustered Disk : No + Serial Number : NVME123 + """; + + if (omitTypeKey) + { + output = string.Join( + Environment.NewLine, + output + .Split(["\r\n", "\n"], StringSplitOptions.None) + .Where(line => !line.TrimStart().StartsWith("Type", StringComparison.OrdinalIgnoreCase))); + } + + return WrapDetailOutput(output, includeBanner, includeUnknownLanguageSelectionText); + } + + private static string WrapDetailOutput( + string detailOutput, + bool includeBanner, + bool includeUnknownLanguageSelectionText) + { + if (includeBanner) + { + return AddBanner(detailOutput, selectionText: "Disk 0 is now the selected disk."); + } + + return includeUnknownLanguageSelectionText + ? AddBanner(detailOutput, selectionText: "LOCALIZED_SELECTION_CONFIRMATION_WITHOUT_COLON") + : detailOutput; + } + + private static string AddBanner(string detailOutput, string selectionText) + { + return $""" + Microsoft DiskPart version 10.0.26100.1 + + Copyright (C) Microsoft Corporation. + On computer: MININT-FOUND + + DISKPART> select disk 0 + + {selectionText} + + DISKPART> detail disk + + {detailOutput} + """; + } + } +} diff --git a/src/Foundry.Deploy.Tests/WindowsDeploymentServiceTests.cs b/src/Foundry.Deploy.Tests/WindowsDeploymentServiceTests.cs index eecc2af2..4516acee 100644 --- a/src/Foundry.Deploy.Tests/WindowsDeploymentServiceTests.cs +++ b/src/Foundry.Deploy.Tests/WindowsDeploymentServiceTests.cs @@ -36,6 +36,72 @@ public async Task PrepareTargetDiskAsync_CreatesPartitionsInExpectedOrder_Efi_Ms Assert.DoesNotContain(scriptLines, line => line.StartsWith("shrink ", StringComparison.OrdinalIgnoreCase)); } + [Fact] + public async Task PrepareTargetDiskAsync_WhenRecoveryMode_PreservesRecoveryAndFormatsWindows() + { + using var workspace = new TemporaryWorkspace(); + string workingDirectory = Path.Combine(workspace.RootPath, "Work"); + var processRunner = new RecordingProcessRunner + { + PowerShellOutput = """ + {"SystemPartitionNumber":1,"RecoveryPartitionNumber":3,"WindowsPartitionNumber":4} + """ + }; + var service = new WindowsDeploymentService(processRunner, NullLogger.Instance); + + await service.PrepareTargetDiskAsync( + 1, + workingDirectory, + RecoveryTargetDiskLayoutMode.RecoveryRetrySafe, + TestContext.Current.CancellationToken); + + string scriptPath = Path.Combine(workingDirectory, "diskpart-os-target.txt"); + string[] scriptLines = await File.ReadAllLinesAsync(scriptPath, TestContext.Current.CancellationToken); + + Assert.DoesNotContain("clean", scriptLines); + Assert.DoesNotContain(scriptLines, line => line.Equals("delete partition override", StringComparison.OrdinalIgnoreCase)); + Assert.Contains("select partition 1", scriptLines); + Assert.Contains("select partition 3", scriptLines); + Assert.Contains("select partition 4", scriptLines); + Assert.Contains("format quick fs=ntfs label=Windows", scriptLines); + Assert.Contains("assign letter=R", scriptLines); + Assert.Contains(processRunner.Calls, call => call.StartsWith("diskpart.exe ", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(processRunner.Calls, call => call.StartsWith("powershell.exe ", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task PrepareTargetDiskAsync_WhenRecoveryMode_UsesPartitionGuidDetails() + { + using var workspace = new TemporaryWorkspace(); + string workingDirectory = Path.Combine(workspace.RootPath, "Work"); + var processRunner = new RecordingProcessRunner + { + DiskPartOutput = """ + Partition ### Type Size Offset + ------------- ---------------- ------- ------- + Partition 1 Système 260 MB 1024 KB + Partition 2 Réservé 16 MB 261 MB + Partition 3 Récupération 5120 MB 277 MB + Partition 4 Principal 470 GB 5397 MB + """ + }; + var service = new WindowsDeploymentService(processRunner, NullLogger.Instance); + + await service.PrepareTargetDiskAsync( + 1, + workingDirectory, + RecoveryTargetDiskLayoutMode.RecoveryRetrySafe, + TestContext.Current.CancellationToken); + + string scriptPath = Path.Combine(workingDirectory, "diskpart-os-target.txt"); + string[] scriptLines = await File.ReadAllLinesAsync(scriptPath, TestContext.Current.CancellationToken); + + Assert.Contains("select partition 1", scriptLines); + Assert.Contains("select partition 3", scriptLines); + Assert.Contains("select partition 4", scriptLines); + Assert.Contains(processRunner.ScriptContents, script => script.Contains("detail partition", StringComparison.OrdinalIgnoreCase)); + } + [Fact] public async Task ConfigureOfflineComputerNameAsync_WhenDefaultTimeZoneIdIsProvided_WritesUnattendTimeZone() { @@ -267,6 +333,17 @@ public Task RunAsync( private sealed class RecordingProcessRunner : IProcessRunner { public List Calls { get; } = []; + public List ScriptContents { get; } = []; + + public string PowerShellOutput { get; init; } = string.Empty; + public string DiskPartOutput { get; init; } = """ + Partition ### Type Size Offset + ------------- ---------------- ------- ------- + Partition 1 System 260 MB 1024 KB + Partition 2 Reserved 16 MB 261 MB + Partition 3 Recovery 5120 MB 277 MB + Partition 4 Primary 470 GB 5397 MB + """; public Task RunAsync( string fileName, @@ -275,7 +352,7 @@ public Task RunAsync( CancellationToken cancellationToken = default) { Calls.Add($"{fileName} {arguments}"); - return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 }); + return Task.FromResult(CreateResult(fileName)); } public Task RunAsync( @@ -285,7 +362,7 @@ public Task RunAsync( CancellationToken cancellationToken = default) { Calls.Add($"{fileName} {string.Join(' ', arguments)}"); - return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 }); + return Task.FromResult(CreateResult(fileName)); } public Task RunAsync( @@ -297,7 +374,50 @@ public Task RunAsync( CancellationToken cancellationToken = default) { Calls.Add($"{fileName} {string.Join(' ', arguments)}"); - return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 }); + return Task.FromResult(CreateResult(fileName)); + } + + private ProcessExecutionResult CreateResult(string fileName) + { + if (string.Equals(fileName, "powershell.exe", StringComparison.OrdinalIgnoreCase)) + { + return new ProcessExecutionResult { ExitCode = 0, StandardOutput = PowerShellOutput }; + } + + if (string.Equals(fileName, "diskpart.exe", StringComparison.OrdinalIgnoreCase)) + { + return new ProcessExecutionResult { ExitCode = 0, StandardOutput = CreateDiskPartOutput() }; + } + + return new ProcessExecutionResult { ExitCode = 0 }; + } + + private string CreateDiskPartOutput() + { + string lastCall = Calls[^1]; + string scriptPath = lastCall.Replace("diskpart.exe /s ", string.Empty, StringComparison.Ordinal).Trim('"'); + string script = File.Exists(scriptPath) ? File.ReadAllText(scriptPath) : string.Empty; + ScriptContents.Add(script); + + if (script.Contains("detail partition", StringComparison.OrdinalIgnoreCase)) + { + if (script.Contains("select partition 1", StringComparison.OrdinalIgnoreCase)) + { + return "Type : c12a7328-f81f-11d2-ba4b-00a0c93ec93b"; + } + + if (script.Contains("select partition 3", StringComparison.OrdinalIgnoreCase)) + { + return "Type : de94bba4-06d1-4d40-a16a-bfd50179d6ac"; + } + + if (script.Contains("select partition 4", StringComparison.OrdinalIgnoreCase)) + { + return "Type : ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"; + } + } + + return DiskPartOutput; } } } diff --git a/src/Foundry.Deploy/DependencyInjection/ServiceCollectionExtensions.cs b/src/Foundry.Deploy/DependencyInjection/ServiceCollectionExtensions.cs index bae634aa..50907b99 100644 --- a/src/Foundry.Deploy/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Foundry.Deploy/DependencyInjection/ServiceCollectionExtensions.cs @@ -23,6 +23,8 @@ using Foundry.Deploy.Services.Theme; using Foundry.Deploy.Services.Wizard; using Foundry.Deploy.ViewModels; +using Foundry.Core.Services.WinPe; +using Foundry.Core.Services.WinPe.OsRecovery; using Foundry.Telemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -47,7 +49,10 @@ public static IServiceCollection AddFoundryDeployApplicationServices(this IServi services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(CreateTelemetryOptions); services.AddSingleton(CreateTelemetryContext); services.AddSingleton(sp => @@ -129,6 +134,7 @@ public static IServiceCollection AddFoundryDeployApplicationServices(this IServi services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Foundry.Deploy/Models/Configuration/DeployOsRecoverySettings.cs b/src/Foundry.Deploy/Models/Configuration/DeployOsRecoverySettings.cs new file mode 100644 index 00000000..1e73532b --- /dev/null +++ b/src/Foundry.Deploy/Models/Configuration/DeployOsRecoverySettings.cs @@ -0,0 +1,12 @@ +namespace Foundry.Deploy.Models.Configuration; + +/// +/// Describes whether Foundry.Deploy should provision the Foundry OS Recovery WinRE entry. +/// +public sealed record DeployOsRecoverySettings +{ + /// + /// Gets a value indicating whether OS Recovery provisioning is enabled. + /// + public bool IsEnabled { get; init; } +} diff --git a/src/Foundry.Deploy/Models/Configuration/FoundryDeployConfigurationDocument.cs b/src/Foundry.Deploy/Models/Configuration/FoundryDeployConfigurationDocument.cs index f2026f68..72ccb423 100644 --- a/src/Foundry.Deploy/Models/Configuration/FoundryDeployConfigurationDocument.cs +++ b/src/Foundry.Deploy/Models/Configuration/FoundryDeployConfigurationDocument.cs @@ -29,6 +29,11 @@ public sealed record FoundryDeployConfigurationDocument /// public DeployLocalizationSettings Localization { get; init; } = new(); + /// + /// Gets Windows RE OS recovery settings used during deployment. + /// + public DeployOsRecoverySettings OsRecovery { get; init; } = new(); + /// /// Gets network profile roaming settings used during deployment. /// diff --git a/src/Foundry.Deploy/Models/DeploymentMode.cs b/src/Foundry.Deploy/Models/DeploymentMode.cs index 3ab93c67..8a4a470b 100644 --- a/src/Foundry.Deploy/Models/DeploymentMode.cs +++ b/src/Foundry.Deploy/Models/DeploymentMode.cs @@ -3,5 +3,6 @@ namespace Foundry.Deploy.Models; public enum DeploymentMode { Usb, - Iso + Iso, + Recovery } diff --git a/src/Foundry.Deploy/Services/Autopilot/IAutopilotHardwareHashCaptureService.cs b/src/Foundry.Deploy/Services/Autopilot/IAutopilotHardwareHashCaptureService.cs index b1016519..98e0673a 100644 --- a/src/Foundry.Deploy/Services/Autopilot/IAutopilotHardwareHashCaptureService.cs +++ b/src/Foundry.Deploy/Services/Autopilot/IAutopilotHardwareHashCaptureService.cs @@ -1,7 +1,7 @@ namespace Foundry.Deploy.Services.Autopilot; /// -/// Captures an Autopilot hardware hash from WinPE without using PowerShell implementation logic. +/// Captures an Autopilot hardware hash from WinPE using OA3Tool. /// public interface IAutopilotHardwareHashCaptureService { diff --git a/src/Foundry.Deploy/Services/Cache/CacheLocatorService.cs b/src/Foundry.Deploy/Services/Cache/CacheLocatorService.cs index 83bb9b7e..f66b3687 100644 --- a/src/Foundry.Deploy/Services/Cache/CacheLocatorService.cs +++ b/src/Foundry.Deploy/Services/Cache/CacheLocatorService.cs @@ -29,6 +29,7 @@ public Task ResolveAsync( CacheResolution resolution = mode switch { DeploymentMode.Iso => ResolveIso(preferredRoot), + DeploymentMode.Recovery => ResolveIso(preferredRoot), _ => ResolveUsb(preferredRoot) }; diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentContext.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentContext.cs index 65ef9a31..ff5d2135 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentContext.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentContext.cs @@ -74,6 +74,11 @@ public sealed record DeploymentContext /// public DeployAutopilotHardwareHashUploadSettings AutopilotHardwareHashUpload { get; init; } = new(); + /// + /// Gets Windows RE OS recovery settings used during deployment. + /// + public DeployOsRecoverySettings OsRecovery { get; init; } = new(); + /// /// Gets network settings used by late deployment steps. /// diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchPreparationService.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchPreparationService.cs index 130f4b1f..9602d6c1 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchPreparationService.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchPreparationService.cs @@ -67,7 +67,7 @@ public DeploymentLaunchPreparationResult Prepare(DeploymentLaunchRequest request return DeploymentLaunchPreparationResult.Failure(normalizedComputerName); } - if (!request.IsDryRun && !ConfirmDestructiveDeployment(effectiveTargetDisk, request.SelectedOperatingSystem)) + if (!request.IsDryRun && !ConfirmDestructiveDeployment(request.Mode, effectiveTargetDisk, request.SelectedOperatingSystem)) { return DeploymentLaunchPreparationResult.Failure(normalizedComputerName); } @@ -87,6 +87,7 @@ public DeploymentLaunchPreparationResult Prepare(DeploymentLaunchRequest request AutopilotProvisioningMode = request.AutopilotProvisioningMode, SelectedAutopilotProfile = request.SelectedAutopilotProfile, AutopilotHardwareHashUpload = request.AutopilotHardwareHashUpload, + OsRecovery = request.OsRecovery, Network = request.Network, Oobe = request.Oobe, AppxRemoval = request.AppxRemoval, @@ -103,15 +104,32 @@ public DeploymentLaunchPreparationResult Prepare(DeploymentLaunchRequest request /// /// Shows the final warning that live deployments erase the selected target disk. /// + /// The deployment mode used to choose the warning text. /// The disk that will be repartitioned. /// The operating system image that will be applied. /// when the user confirms the destructive operation. - private bool ConfirmDestructiveDeployment(TargetDiskInfo targetDisk, OperatingSystemCatalogItem operatingSystem) + private bool ConfirmDestructiveDeployment( + DeploymentMode mode, + TargetDiskInfo targetDisk, + OperatingSystemCatalogItem operatingSystem) { string sizeGiB = targetDisk.SizeBytes > 0 ? $"{(targetDisk.SizeBytes / 1024d / 1024d / 1024d):0.0} GiB" : LocalizationText.GetString("Disk.UnknownSize"); + if (mode == DeploymentMode.Recovery) + { + string recoveryMessage = LocalizationText.Format( + "Launch.ConfirmOsRecoveryMessageFormat", + targetDisk.DiskNumber, + targetDisk.FriendlyName, + targetDisk.BusType, + sizeGiB, + operatingSystem.DisplayLabel); + + return _applicationShellService.ConfirmWarning(LocalizationText.GetString("Launch.ConfirmOsRecoveryTitle"), recoveryMessage); + } + string message = LocalizationText.Format( "Launch.ConfirmDiskEraseMessageFormat", targetDisk.DiskNumber, diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchRequest.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchRequest.cs index 8e3b02db..06428458 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchRequest.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentLaunchRequest.cs @@ -19,6 +19,7 @@ public sealed record DeploymentLaunchRequest public required AutopilotProvisioningMode AutopilotProvisioningMode { get; init; } public required AutopilotProfileCatalogItem? SelectedAutopilotProfile { get; init; } public DeployAutopilotHardwareHashUploadSettings AutopilotHardwareHashUpload { get; init; } = new(); + public DeployOsRecoverySettings OsRecovery { get; init; } = new(); public CoreDeployNetworkSettings Network { get; init; } = new(); public DeployOobeSettings Oobe { get; init; } = new(); public DeployAppxRemovalSettings AppxRemoval { get; init; } = new(); diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentOrchestrator.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentOrchestrator.cs index c285736b..a10912d4 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentOrchestrator.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentOrchestrator.cs @@ -122,6 +122,7 @@ await TrackDeploymentCompletedAsync( AutopilotHardwareHashUploadState = context.IsAutopilotEnabled && context.AutopilotProvisioningMode == AutopilotProvisioningMode.HardwareHashUpload ? AutopilotHardwareHashUploadState.Planned : AutopilotHardwareHashUploadState.NotPlanned, + IsOsRecoveryEnabled = context.OsRecovery.IsEnabled, Network = context.Network, Oobe = context.Oobe, AppxRemoval = context.AppxRemoval, @@ -291,6 +292,7 @@ private Task TrackDeploymentCompletedAsync( ["deploy_driver_pack_vendor"] = NormalizeTelemetryString(context.DriverPack?.Manufacturer, "none"), ["deploy_driver_pack_model"] = ResolveDriverPackCatalogModel(context.DriverPack), ["deploy_firmware_updates_enabled"] = context.ApplyFirmwareUpdates, + ["deploy_os_recovery_enabled"] = context.OsRecovery.IsEnabled, ["deploy_autopilot_enabled"] = context.IsAutopilotEnabled, ["deploy_autopilot_provisioning_mode"] = NormalizeTelemetryString(ResolveAutopilotProvisioningMode(context)), ["deploy_autopilot_hash_upload_state"] = NormalizeTelemetryString(runtimeState?.AutopilotHardwareHashUploadState.ToString()), diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentRuntimeState.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentRuntimeState.cs index 9e2fd549..3be26fa9 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentRuntimeState.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentRuntimeState.cs @@ -157,7 +157,7 @@ public sealed record DeploymentRuntimeState public string? PreOobeSetupCompletePath { get; set; } /// - /// Gets or sets the offline path to the generated pre-OOBE PowerShell runner. + /// Gets or sets the offline path to the generated pre-OOBE runner. /// public string? PreOobeRunnerPath { get; set; } @@ -167,7 +167,7 @@ public sealed record DeploymentRuntimeState public string? PreOobeManifestPath { get; set; } /// - /// Gets or sets the offline paths to staged pre-OOBE PowerShell scripts. + /// Gets or sets the offline paths to staged pre-OOBE scripts. /// public IReadOnlyList PreOobeScriptPaths { get; set; } = []; @@ -236,6 +236,11 @@ public sealed record DeploymentRuntimeState /// public string? AutopilotHardwareHashUploadMessage { get; set; } + /// + /// Gets or sets a value indicating whether Foundry OS Recovery WinRE provisioning is enabled. + /// + public bool IsOsRecoveryEnabled { get; set; } + /// /// Gets or sets the diagnostics directory retained under the target Windows Temp\Foundry tree. /// diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentStepExecutionContext.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentStepExecutionContext.cs index f697c4e4..66916eb8 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentStepExecutionContext.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentStepExecutionContext.cs @@ -250,7 +250,7 @@ await _deploymentLogService return (null, DeploymentStepResult.Failed($"Target disk {Request.TargetDiskNumber} is no longer present.")); } - if (!selectedDisk.IsSelectable) + if (!selectedDisk.IsSelectable && RuntimeState.Mode != DeploymentMode.Recovery) { return (null, DeploymentStepResult.Failed( $"Target disk {Request.TargetDiskNumber} is blocked: {selectedDisk.SelectionWarning}")); @@ -512,7 +512,7 @@ private string EnsureCacheBaseRoot() private string ResolvePayloadCacheRoot(string payloadFolderName, long requiredBytes) { - if (RuntimeState.Mode == DeploymentMode.Iso && + if ((RuntimeState.Mode == DeploymentMode.Iso || RuntimeState.Mode == DeploymentMode.Recovery) && !string.IsNullOrWhiteSpace(RuntimeState.TargetFoundryRoot)) { return Path.Combine(RuntimeState.TargetFoundryRoot, CacheFolderName, payloadFolderName); diff --git a/src/Foundry.Deploy/Services/Deployment/DeploymentStepNames.cs b/src/Foundry.Deploy/Services/Deployment/DeploymentStepNames.cs index f99e32de..a1f40816 100644 --- a/src/Foundry.Deploy/Services/Deployment/DeploymentStepNames.cs +++ b/src/Foundry.Deploy/Services/Deployment/DeploymentStepNames.cs @@ -19,6 +19,7 @@ public static class DeploymentStepNames public const string ConfigureRecoveryEnvironment = "Configure recovery environment"; public const string StagePreOobeCustomization = "Stage pre-OOBE customization"; public const string ApplyDriverPack = "Apply driver pack"; + public const string ProvisionOsRecovery = "Provision OS Recovery"; public const string DownloadFirmwareUpdate = "Download firmware update"; public const string ApplyFirmwareUpdate = "Apply firmware update"; public const string SealRecoveryPartition = "Seal recovery partition"; @@ -44,6 +45,7 @@ public static class DeploymentStepNames ExtractDriverPack, StagePreOobeCustomization, ApplyDriverPack, + ProvisionOsRecovery, DownloadFirmwareUpdate, ApplyFirmwareUpdate, SealRecoveryPartition, diff --git a/src/Foundry.Deploy/Services/Deployment/IWindowsDeploymentService.cs b/src/Foundry.Deploy/Services/Deployment/IWindowsDeploymentService.cs index 914e938b..43849089 100644 --- a/src/Foundry.Deploy/Services/Deployment/IWindowsDeploymentService.cs +++ b/src/Foundry.Deploy/Services/Deployment/IWindowsDeploymentService.cs @@ -19,6 +19,20 @@ Task PrepareTargetDiskAsync( string workingDirectory, CancellationToken cancellationToken = default); + /// + /// Prepares the target disk with the requested layout strategy. + /// + /// The disk number to prepare. + /// The directory used for temporary scripts. + /// The layout strategy to apply. + /// A token used to cancel diskpart execution. + /// The resulting target partition layout. + Task PrepareTargetDiskAsync( + int diskNumber, + string workingDirectory, + RecoveryTargetDiskLayoutMode layoutMode, + CancellationToken cancellationToken = default); + /// /// Resolves the WIM/ESD image index matching a requested edition. /// diff --git a/src/Foundry.Deploy/Services/Deployment/RecoveryTargetDiskLayoutMode.cs b/src/Foundry.Deploy/Services/Deployment/RecoveryTargetDiskLayoutMode.cs new file mode 100644 index 00000000..70ab2c74 --- /dev/null +++ b/src/Foundry.Deploy/Services/Deployment/RecoveryTargetDiskLayoutMode.cs @@ -0,0 +1,7 @@ +namespace Foundry.Deploy.Services.Deployment; + +public enum RecoveryTargetDiskLayoutMode +{ + FullWipe, + RecoveryRetrySafe +} diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/ApplyFirmwareUpdateStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/ApplyFirmwareUpdateStep.cs index 9410b8f0..42a4309d 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/ApplyFirmwareUpdateStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/ApplyFirmwareUpdateStep.cs @@ -12,7 +12,7 @@ public ApplyFirmwareUpdateStep(IWindowsDeploymentService windowsDeploymentServic _windowsDeploymentService = windowsDeploymentService; } - public override int Order => 16; + public override int Order => 17; public override string Name => DeploymentStepNames.ApplyFirmwareUpdate; diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/DownloadFirmwareUpdateStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/DownloadFirmwareUpdateStep.cs index 2e87b7f5..ac8914e5 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/DownloadFirmwareUpdateStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/DownloadFirmwareUpdateStep.cs @@ -14,7 +14,7 @@ public DownloadFirmwareUpdateStep(IMicrosoftUpdateCatalogFirmwareService firmwar _firmwareService = firmwareService; } - public override int Order => 15; + public override int Order => 16; public override string Name => DeploymentStepNames.DownloadFirmwareUpdate; diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/FinalizeDeploymentAndWriteLogsStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/FinalizeDeploymentAndWriteLogsStep.cs index 3e1f5c9e..efd014bd 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/FinalizeDeploymentAndWriteLogsStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/FinalizeDeploymentAndWriteLogsStep.cs @@ -6,7 +6,7 @@ namespace Foundry.Deploy.Services.Deployment.Steps; public sealed class FinalizeDeploymentAndWriteLogsStep : DeploymentStepBase { - public override int Order => 19; + public override int Order => 20; public override string Name => DeploymentStepNames.FinalizeDeploymentAndWriteLogs; @@ -98,6 +98,7 @@ private static async Task WriteDeploymentSummaryAsync( preOobeManifestPath = runtimeState.PreOobeManifestPath, preOobeScriptPaths = runtimeState.PreOobeScriptPaths, applyFirmwareUpdates = runtimeState.ApplyFirmwareUpdates, + osRecoveryEnabled = runtimeState.IsOsRecoveryEnabled, downloadedFirmwarePath = runtimeState.DownloadedFirmwarePath, extractedFirmwarePath = runtimeState.ExtractedFirmwarePath, firmwareUpdateId = runtimeState.FirmwareUpdateId, diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/GatherDeploymentVariablesStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/GatherDeploymentVariablesStep.cs index 83c431d2..2317de13 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/GatherDeploymentVariablesStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/GatherDeploymentVariablesStep.cs @@ -38,6 +38,10 @@ private static Task AppendRunContextAsync(DeploymentStepExecutionContext context targetComputerName = context.Request.TargetComputerName, driverPackSelectionKind = context.Request.DriverPackSelectionKind.ToString(), applyFirmwareUpdates = context.Request.ApplyFirmwareUpdates, + osRecovery = new + { + isEnabled = context.Request.OsRecovery.IsEnabled + }, autopilot = new { isEnabled = context.Request.IsAutopilotEnabled, @@ -110,6 +114,7 @@ private static Task AppendRunContextAsync(DeploymentStepExecutionContext context context.RuntimeState.ExtractedFirmwarePath, context.RuntimeState.FirmwareUpdateId, context.RuntimeState.FirmwareUpdateTitle, + context.RuntimeState.IsOsRecoveryEnabled, context.RuntimeState.IsAutopilotEnabled, context.RuntimeState.SelectedAutopilotProfileFolderName, context.RuntimeState.SelectedAutopilotProfileDisplayName, diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/PrepareTargetDiskLayoutStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/PrepareTargetDiskLayoutStep.cs index 43d9c4b4..6f65eb02 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/PrepareTargetDiskLayoutStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/PrepareTargetDiskLayoutStep.cs @@ -30,10 +30,15 @@ protected override async Task ExecuteLiveAsync(DeploymentS } context.EmitCurrentStepIndeterminate("Preparing target disk layout...", "Partitioning target disk..."); + RecoveryTargetDiskLayoutMode layoutMode = context.RuntimeState.Mode == DeploymentMode.Recovery + ? RecoveryTargetDiskLayoutMode.RecoveryRetrySafe + : RecoveryTargetDiskLayoutMode.FullWipe; + DeploymentTargetLayout layout = await _windowsDeploymentService .PrepareTargetDiskAsync( context.Request.TargetDiskNumber, workingDirectory, + layoutMode, cancellationToken) .ConfigureAwait(false); diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionAutopilotStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionAutopilotStep.cs index 4bf27a1e..e64faf27 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionAutopilotStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionAutopilotStep.cs @@ -30,7 +30,7 @@ public ProvisionAutopilotStep( _interactiveRegistrationProvisioningService = interactiveRegistrationProvisioningService; } - public override int Order => 18; + public override int Order => 19; public override string Name => DeploymentStepNames.ProvisionAutopilot; diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionOsRecoveryStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionOsRecoveryStep.cs new file mode 100644 index 00000000..093b681b --- /dev/null +++ b/src/Foundry.Deploy/Services/Deployment/Steps/ProvisionOsRecoveryStep.cs @@ -0,0 +1,470 @@ +using System.Globalization; +using System.IO; +using System.Text.Json; +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; +using Foundry.Core.Services.WinPe; +using Foundry.Core.Services.WinPe.OsRecovery; +using Foundry.Deploy.Models; +using Foundry.Deploy.Models.Configuration; +using Foundry.Deploy.Services.Configuration; +using Foundry.Deploy.Services.Localization; +using Foundry.Deploy.Services.Logging; +using Foundry.Deploy.Services.System; +using CoreDeployNetworkSettings = Foundry.Core.Models.Configuration.Deploy.DeployNetworkSettings; + +namespace Foundry.Deploy.Services.Deployment.Steps; + +public sealed class ProvisionOsRecoveryStep : DeploymentStepBase +{ + private const string ConnectConfigurationPath = @"X:\Foundry\Config\foundry.connect.config.json"; + private const string WinReImageFileName = "winre.wim"; + private const string RecoveryMarkerFileName = "FoundryOsRecovery.json"; + + private readonly IOsRecoveryPayloadProvisioningService _osRecoveryPayloadProvisioningService; + private readonly IWinPeEmbeddedAssetService _embeddedAssetService; + private readonly IDeployConfigurationService _deployConfigurationService; + private readonly IProcessRunner _processRunner; + private readonly string? _winReConfigToolPath; + + public ProvisionOsRecoveryStep( + IOsRecoveryPayloadProvisioningService osRecoveryPayloadProvisioningService, + IWinPeEmbeddedAssetService embeddedAssetService, + IDeployConfigurationService deployConfigurationService, + IProcessRunner processRunner) + : this( + osRecoveryPayloadProvisioningService, + embeddedAssetService, + deployConfigurationService, + processRunner, + winReConfigToolPath: null) + { + } + + internal ProvisionOsRecoveryStep( + IOsRecoveryPayloadProvisioningService osRecoveryPayloadProvisioningService, + IWinPeEmbeddedAssetService embeddedAssetService, + IDeployConfigurationService deployConfigurationService, + IProcessRunner processRunner, + string? winReConfigToolPath) + { + _osRecoveryPayloadProvisioningService = osRecoveryPayloadProvisioningService; + _embeddedAssetService = embeddedAssetService; + _deployConfigurationService = deployConfigurationService; + _processRunner = processRunner; + _winReConfigToolPath = winReConfigToolPath; + } + + public override int Order => 15; + + public override string Name => DeploymentStepNames.ProvisionOsRecovery; + + protected override async Task ExecuteLiveAsync( + DeploymentStepExecutionContext context, + CancellationToken cancellationToken) + { + if (!context.Request.OsRecovery.IsEnabled) + { + return DeploymentStepResult.Skipped("OS Recovery is disabled."); + } + + if (context.RuntimeState.Mode == DeploymentMode.Recovery) + { + return DeploymentStepResult.Skipped("OS Recovery provisioning is skipped in recovery mode."); + } + + if (string.IsNullOrWhiteSpace(context.RuntimeState.TargetWindowsPartitionRoot) || + string.IsNullOrWhiteSpace(context.RuntimeState.TargetRecoveryPartitionRoot) || + !context.RuntimeState.WinReConfigured) + { + return DeploymentStepResult.Failed("Recovery partition is unavailable."); + } + + string targetFoundryRoot = context.EnsureTargetFoundryRoot(); + string workingDirectory = Path.Combine(targetFoundryRoot, "Temp", "Deployment", "OsRecovery"); + string scratchDirectory = Path.Combine(targetFoundryRoot, "Temp", "Dism"); + string mountPath = Path.Combine(workingDirectory, "Mount-WindowsRE"); + string winReImagePath = ResolveWinReImagePath(context.RuntimeState.TargetRecoveryPartitionRoot); + string recoveryMarkerPath = ResolveRecoveryMarkerPath(context.RuntimeState.TargetRecoveryPartitionRoot); + string windowsPath = Path.Combine(context.RuntimeState.TargetWindowsPartitionRoot, "Windows"); + + Directory.CreateDirectory(workingDirectory); + Directory.CreateDirectory(scratchDirectory); + ResetDirectory(mountPath); + + string deployConfigurationJson = await ReadDeployConfigurationJsonAsync(cancellationToken).ConfigureAwait(false); + string connectConfigurationJson = await ReadRecoveryConnectConfigurationJsonAsync(cancellationToken).ConfigureAwait(false); + + context.EmitCurrentStepIndeterminate("Provisioning OS Recovery...", "Mounting WinRE..."); + await RunRequiredProcessAsync( + "dism.exe", + [ + "/Mount-Image", + $"/ImageFile:{winReImagePath}", + "/Index:1", + $"/MountDir:{mountPath}", + $"/ScratchDir:{scratchDirectory}" + ], + workingDirectory, + "Failed to mount the Windows RE image", + cancellationToken).ConfigureAwait(false); + + bool mounted = true; + bool shouldCommit = false; + bool markerWritten = false; + Exception? operationException = null; + try + { + context.EmitCurrentStepIndeterminate("Provisioning OS Recovery...", "Injecting OS Recovery payload..."); + WinPeResult provisioningResult = + await _osRecoveryPayloadProvisioningService.ProvisionAsync( + new OsRecoveryPayloadProvisioningOptions + { + MountedImagePath = mountPath, + WorkingDirectoryPath = Path.Combine(workingDirectory, "Payload"), + Architecture = ResolveArchitecture(context.Request.OperatingSystem.Architecture), + FoundryConnectConfigurationJson = connectConfigurationJson, + DeployConfigurationJson = deployConfigurationJson, + IanaWindowsTimeZoneMapJson = _embeddedAssetService.GetIanaWindowsTimeZoneMapJson(), + CurlExecutableSourcePath = ResolveCurlExecutablePath(), + SevenZipSourceDirectoryPath = _embeddedAssetService.GetSevenZipSourceDirectoryPath(), + Connect = new WinPeRuntimePayloadApplicationOptions + { + IsEnabled = true, + ProvisioningSource = WinPeProvisioningSource.Release + }, + BootMenuLocalizations = CreateBootMenuLocalizations() + }, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (!provisioningResult.IsSuccess) + { + throw new InvalidOperationException(FormatDiagnostic(provisioningResult.Error)); + } + + shouldCommit = true; + + context.EmitCurrentStepIndeterminate("Provisioning OS Recovery...", "Committing WinRE changes..."); + await UnmountAsync(mountPath, workingDirectory, shouldCommit: true, cancellationToken).ConfigureAwait(false); + mounted = false; + + context.EmitCurrentStepIndeterminate("Provisioning OS Recovery...", "Registering OS Recovery boot menu..."); + string bootMenuConfigPath = Path.Combine(workingDirectory, "AddFoundryRecoveryToBootMenu.xml"); + await File.WriteAllTextAsync( + bootMenuConfigPath, + provisioningResult.Value!.BootMenuConfigurationXml, + cancellationToken).ConfigureAwait(false); + + await WriteRecoveryMarkerAsync(recoveryMarkerPath, context, provisioningResult.Value, cancellationToken).ConfigureAwait(false); + markerWritten = true; + + await RunRequiredProcessAsync( + ResolveRequiredWinReConfigToolPath(), + ["/setbootshelllink", "/configfile", bootMenuConfigPath, "/target", windowsPath], + workingDirectory, + "Failed to register the OS Recovery boot menu link", + cancellationToken).ConfigureAwait(false); + + await context.AppendLogAsync( + DeploymentLogLevel.Info, + $"OS Recovery provisioned. ManagedPayloadSizeBytes={provisioningResult.Value.ManagedPayloadSizeBytes}.", + cancellationToken).ConfigureAwait(false); + + return DeploymentStepResult.Succeeded("OS Recovery provisioned."); + } + catch (Exception ex) + { + operationException = ex; + if (markerWritten) + { + TryDeleteFile(recoveryMarkerPath); + } + + throw; + } + finally + { + if (mounted) + { + try + { + await UnmountAsync(mountPath, workingDirectory, shouldCommit, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception unmountException) when (operationException is not null) + { + await TryAppendCleanupFailureLogAsync(context, unmountException).ConfigureAwait(false); + } + } + + TryDeleteDirectory(mountPath); + } + } + + protected override async Task ExecuteDryRunAsync( + DeploymentStepExecutionContext context, + CancellationToken cancellationToken) + { + if (!context.Request.OsRecovery.IsEnabled) + { + return DeploymentStepResult.Skipped("OS Recovery is disabled."); + } + + if (context.RuntimeState.Mode == DeploymentMode.Recovery) + { + return DeploymentStepResult.Skipped("OS Recovery provisioning is skipped in recovery mode."); + } + + context.EmitCurrentStepIndeterminate("Provisioning OS Recovery...", "Injecting OS Recovery payload..."); + await context.AppendLogAsync( + DeploymentLogLevel.Info, + "[DRY-RUN] Simulated OS Recovery WinRE payload provisioning.", + cancellationToken).ConfigureAwait(false); + await Task.Delay(150, cancellationToken).ConfigureAwait(false); + + return DeploymentStepResult.Succeeded("OS Recovery provisioned (simulation)."); + } + + private async Task ReadDeployConfigurationJsonAsync(CancellationToken cancellationToken) + { + DeployConfigurationLoadResult loadResult = _deployConfigurationService.LoadOptional(); + if (!loadResult.Exists || string.IsNullOrWhiteSpace(loadResult.ConfigurationPath) || !File.Exists(loadResult.ConfigurationPath)) + { + throw new FileNotFoundException("The Foundry.Deploy configuration file is required for OS Recovery provisioning.", loadResult.ConfigurationPath); + } + + await using FileStream stream = File.OpenRead(loadResult.ConfigurationPath); + FoundryDeployConfigurationDocument? document = await JsonSerializer.DeserializeAsync( + stream, + ConfigurationJsonDefaults.SerializerOptions, + cancellationToken).ConfigureAwait(false); + + if (document is null) + { + throw new InvalidOperationException("The Foundry.Deploy configuration file could not be parsed for OS Recovery provisioning."); + } + + FoundryDeployConfigurationDocument recoveryDocument = document with + { + Autopilot = new DeployAutopilotSettings(), + Network = new CoreDeployNetworkSettings() + }; + + return JsonSerializer.Serialize(recoveryDocument, ConfigurationJsonDefaults.SerializerOptions); + } + + private static async Task ReadRecoveryConnectConfigurationJsonAsync(CancellationToken cancellationToken) + { + if (!File.Exists(ConnectConfigurationPath)) + { + return JsonSerializer.Serialize(new FoundryConnectConfigurationDocument(), ConfigurationJsonDefaults.SerializerOptions); + } + + await using FileStream stream = File.OpenRead(ConnectConfigurationPath); + FoundryConnectConfigurationDocument? document = await JsonSerializer.DeserializeAsync( + stream, + ConfigurationJsonDefaults.SerializerOptions, + cancellationToken).ConfigureAwait(false); + + document ??= new FoundryConnectConfigurationDocument(); + FoundryConnectConfigurationDocument recoveryDocument = new() + { + SchemaVersion = document.SchemaVersion, + InternetProbe = document.InternetProbe, + Telemetry = document.Telemetry + }; + + return JsonSerializer.Serialize(recoveryDocument, ConfigurationJsonDefaults.SerializerOptions); + } + + private static IReadOnlyList CreateBootMenuLocalizations() + { + return new EmbeddedLanguageRegistryService() + .GetLanguages() + .Select(language => + { + CultureInfo culture = CultureInfo.GetCultureInfo(language.Code); + return new OsRecoveryBootMenuLocalization + { + Culture = language.Code, + Name = LocalizationText.ResourceManager.GetString("OsRecovery.BootMenuName", culture) ?? "Foundry Recovery", + Description = LocalizationText.ResourceManager.GetString("OsRecovery.BootMenuDescription", culture) ?? "Redeploy Windows" + }; + }) + .ToArray(); + } + + private async Task UnmountAsync( + string mountPath, + string workingDirectory, + bool shouldCommit, + CancellationToken cancellationToken) + { + await RunRequiredProcessAsync( + "dism.exe", + shouldCommit + ? ["/Unmount-Image", $"/MountDir:{mountPath}", "/Commit"] + : ["/Unmount-Image", $"/MountDir:{mountPath}", "/Discard"], + workingDirectory, + shouldCommit + ? "Failed to commit the Windows RE image" + : "Failed to discard the Windows RE image", + cancellationToken).ConfigureAwait(false); + } + + private static async Task TryAppendCleanupFailureLogAsync( + DeploymentStepExecutionContext context, + Exception exception) + { + try + { + await context.AppendLogAsync( + DeploymentLogLevel.Warning, + $"Failed to clean up mounted WinRE image after OS Recovery provisioning failure. {exception.Message}", + CancellationToken.None).ConfigureAwait(false); + } + catch + { + } + } + + private async Task RunRequiredProcessAsync( + string fileName, + IEnumerable arguments, + string workingDirectory, + string failureMessage, + CancellationToken cancellationToken) + { + ProcessExecutionResult result = await _processRunner + .RunAsync(fileName, arguments, workingDirectory, cancellationToken) + .ConfigureAwait(false); + + if (result.IsSuccess) + { + return; + } + + throw new InvalidOperationException($"{failureMessage}.{Environment.NewLine}{ToDiagnostic(result)}"); + } + + private static WinPeArchitecture ResolveArchitecture(string? architecture) + { + return !string.IsNullOrWhiteSpace(architecture) && + architecture.Contains("arm", StringComparison.OrdinalIgnoreCase) + ? WinPeArchitecture.Arm64 + : WinPeArchitecture.X64; + } + + private static string ResolveCurlExecutablePath() + { + return Path.Combine(Environment.SystemDirectory, "curl.exe"); + } + + private static string ResolveWinReImagePath(string recoveryPartitionRoot) + { + return Path.Combine(recoveryPartitionRoot, "Recovery", "WindowsRE", WinReImageFileName); + } + + private static string ResolveRecoveryMarkerPath(string recoveryPartitionRoot) + { + return Path.Combine(recoveryPartitionRoot, "Recovery", "WindowsRE", RecoveryMarkerFileName); + } + + private static Task WriteRecoveryMarkerAsync( + string markerPath, + DeploymentStepExecutionContext context, + OsRecoveryPayloadProvisioningResult provisioningResult, + CancellationToken cancellationToken) + { + Directory.CreateDirectory(Path.GetDirectoryName(markerPath)!); + var marker = new + { + schemaVersion = 1, + product = "Foundry OS Recovery", + createdUtc = DateTimeOffset.UtcNow, + targetDiskNumber = context.RuntimeState.TargetDiskNumber, + managedPayloadSizeBytes = provisioningResult.ManagedPayloadSizeBytes + }; + + string json = JsonSerializer.Serialize(marker, ConfigurationJsonDefaults.SerializerOptions); + return File.WriteAllTextAsync(markerPath, json, cancellationToken); + } + + private string ResolveRequiredWinReConfigToolPath() + { + if (!string.IsNullOrWhiteSpace(_winReConfigToolPath)) + { + return _winReConfigToolPath; + } + + string path = Path.Combine(Environment.SystemDirectory, "winrecfg.exe"); + if (!File.Exists(path)) + { + throw new FileNotFoundException( + "Required WinPE executable 'winrecfg.exe' was not found. Add the WinPE-WinReCfg optional component to the WinPE image.", + path); + } + + return path; + } + + private static void ResetDirectory(string path) + { + TryDeleteDirectory(path); + Directory.CreateDirectory(path); + } + + private static void TryDeleteDirectory(string path) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } + catch + { + } + } + + private static void TryDeleteFile(string path) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + } + } + + private static string FormatDiagnostic(WinPeDiagnostic? diagnostic) + { + if (diagnostic is null) + { + return "OS Recovery payload provisioning failed."; + } + + return string.IsNullOrWhiteSpace(diagnostic.Details) + ? diagnostic.Message + : $"{diagnostic.Message}{Environment.NewLine}{diagnostic.Details}"; + } + + private static string ToDiagnostic(ProcessExecutionResult result) + { + string stdout = string.IsNullOrWhiteSpace(result.StandardOutput) ? string.Empty : result.StandardOutput.Trim(); + string stderr = string.IsNullOrWhiteSpace(result.StandardError) ? string.Empty : result.StandardError.Trim(); + + return string.Join( + Environment.NewLine, + [ + $"Command: {result.FileName} {result.Arguments}", + $"ExitCode: {result.ExitCode}", + stdout.Length == 0 ? "StdOut: " : $"StdOut: {stdout}", + stderr.Length == 0 ? "StdErr: " : $"StdErr: {stderr}" + ]); + } +} diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/SealRecoveryPartitionStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/SealRecoveryPartitionStep.cs index 13559757..80eadd6e 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/SealRecoveryPartitionStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/SealRecoveryPartitionStep.cs @@ -12,7 +12,7 @@ public SealRecoveryPartitionStep(IWindowsDeploymentService windowsDeploymentServ _windowsDeploymentService = windowsDeploymentService; } - public override int Order => 17; + public override int Order => 18; public override string Name => DeploymentStepNames.SealRecoveryPartition; diff --git a/src/Foundry.Deploy/Services/Deployment/WindowsDeploymentService.cs b/src/Foundry.Deploy/Services/Deployment/WindowsDeploymentService.cs index c68e5d88..564098c4 100644 --- a/src/Foundry.Deploy/Services/Deployment/WindowsDeploymentService.cs +++ b/src/Foundry.Deploy/Services/Deployment/WindowsDeploymentService.cs @@ -18,7 +18,9 @@ public sealed class WindowsDeploymentService : IWindowsDeploymentService private const int MsrPartitionSizeMb = 16; private const int RecoveryPartitionSizeMb = 5120; private const string RecoveryPartitionLabel = "Recovery"; + private const string EfiPartitionGuid = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"; private const string RecoveryPartitionGuid = "de94bba4-06d1-4d40-a16a-bfd50179d6ac"; + private const string BasicDataPartitionGuid = "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"; private const string RecoveryPartitionAttributes = "0x8000000000000001"; private const string WinReImageFileName = "winre.wim"; private readonly IProcessRunner _processRunner; @@ -46,12 +48,31 @@ public async Task PrepareTargetDiskAsync( int diskNumber, string workingDirectory, CancellationToken cancellationToken = default) + { + return await PrepareTargetDiskAsync( + diskNumber, + workingDirectory, + RecoveryTargetDiskLayoutMode.FullWipe, + cancellationToken).ConfigureAwait(false); + } + + /// + public async Task PrepareTargetDiskAsync( + int diskNumber, + string workingDirectory, + RecoveryTargetDiskLayoutMode layoutMode, + CancellationToken cancellationToken = default) { if (diskNumber < 0) { throw new ArgumentOutOfRangeException(nameof(diskNumber), "Target disk number must be 0 or greater."); } + if (layoutMode == RecoveryTargetDiskLayoutMode.RecoveryRetrySafe) + { + return await PrepareRecoveryRetrySafeTargetDiskAsync(diskNumber, workingDirectory, cancellationToken).ConfigureAwait(false); + } + _logger.LogInformation( "Preparing target disk layout. DiskNumber={DiskNumber}, RecoveryPartitionSizeMb={RecoveryPartitionSizeMb}, WorkingDirectory={WorkingDirectory}", diskNumber, @@ -113,6 +134,141 @@ await RunRequiredProcessAsync( }; } + private async Task PrepareRecoveryRetrySafeTargetDiskAsync( + int diskNumber, + string workingDirectory, + CancellationToken cancellationToken) + { + Directory.CreateDirectory(workingDirectory); + (char systemLetter, char windowsLetter, char recoveryLetter) = GetPartitionLetters(); + + RecoveryDiskLayoutProbe probe = await ProbeRecoveryDiskLayoutAsync(diskNumber, workingDirectory, cancellationToken).ConfigureAwait(false); + + string[] scriptLines = + [ + $"select disk {diskNumber}", + "online disk noerr", + "attributes disk clear readonly noerr", + $"select partition {probe.SystemPartitionNumber}", + $"assign letter={systemLetter}", + $"select partition {probe.RecoveryPartitionNumber}", + $"assign letter={recoveryLetter}", + $"select partition {probe.WindowsPartitionNumber}", + "format quick fs=ntfs label=Windows", + $"assign letter={windowsLetter}" + ]; + + string scriptPath = Path.Combine(workingDirectory, "diskpart-os-target.txt"); + await File.WriteAllLinesAsync(scriptPath, scriptLines, cancellationToken).ConfigureAwait(false); + + await RunRequiredProcessAsync( + "diskpart.exe", + $"/s \"{scriptPath}\"", + workingDirectory, + $"Recovery disk preparation failed for disk {diskNumber}", + cancellationToken).ConfigureAwait(false); + + return new DeploymentTargetLayout + { + DiskNumber = diskNumber, + SystemPartitionRoot = $"{systemLetter}:\\", + WindowsPartitionRoot = $"{windowsLetter}:\\", + RecoveryPartitionRoot = $"{recoveryLetter}:\\", + RecoveryPartitionLetter = recoveryLetter + }; + } + + private async Task ProbeRecoveryDiskLayoutAsync( + int diskNumber, + string workingDirectory, + CancellationToken cancellationToken) + { + string scriptPath = Path.Combine(workingDirectory, "diskpart-recovery-probe.txt"); + await File.WriteAllLinesAsync( + scriptPath, + [ + $"select disk {diskNumber}", + "list partition" + ], + cancellationToken).ConfigureAwait(false); + + ProcessExecutionResult execution = await _processRunner + .RunAsync("diskpart.exe", $"/s \"{scriptPath}\"", workingDirectory, cancellationToken) + .ConfigureAwait(false); + + if (!execution.IsSuccess || string.IsNullOrWhiteSpace(execution.StandardOutput)) + { + throw new InvalidOperationException( + $"Unable to validate recovery disk layout for disk {diskNumber}.{Environment.NewLine}{ToDiagnostic(execution)}"); + } + + IReadOnlyList partitions = DiskPartOutputParser.ParseListPartition(execution.StandardOutput); + Dictionary partitionTypeGuids = await ReadPartitionTypeGuidsAsync( + diskNumber, + partitions, + workingDirectory, + cancellationToken).ConfigureAwait(false); + + DiskPartPartition? system = partitions.FirstOrDefault(partition => IsPartitionType(partition, partitionTypeGuids, EfiPartitionGuid)); + DiskPartPartition? recovery = partitions.FirstOrDefault(partition => IsPartitionType(partition, partitionTypeGuids, RecoveryPartitionGuid)); + DiskPartPartition? windows = recovery is null + ? null + : partitions.FirstOrDefault(partition => + partition.Number > recovery.Number && + IsPartitionType(partition, partitionTypeGuids, BasicDataPartitionGuid)); + + if (system is null || recovery is null || windows is null || recovery.Number >= windows.Number) + { + throw new InvalidOperationException( + $"The target disk layout is not supported for OS Recovery. Expected EFI, MSR, Recovery, Windows on disk {diskNumber}."); + } + + return new RecoveryDiskLayoutProbe + { + SystemPartitionNumber = system.Number, + RecoveryPartitionNumber = recovery.Number, + WindowsPartitionNumber = windows.Number + }; + } + + private async Task> ReadPartitionTypeGuidsAsync( + int diskNumber, + IReadOnlyList partitions, + string workingDirectory, + CancellationToken cancellationToken) + { + var result = new Dictionary(); + foreach (DiskPartPartition partition in partitions) + { + string scriptPath = Path.Combine(workingDirectory, $"diskpart-partition-{partition.Number}-detail.txt"); + await File.WriteAllLinesAsync( + scriptPath, + [ + $"select disk {diskNumber}", + $"select partition {partition.Number}", + "detail partition" + ], + cancellationToken).ConfigureAwait(false); + + ProcessExecutionResult execution = await _processRunner + .RunAsync("diskpart.exe", $"/s \"{scriptPath}\"", workingDirectory, cancellationToken) + .ConfigureAwait(false); + + if (!execution.IsSuccess || string.IsNullOrWhiteSpace(execution.StandardOutput)) + { + continue; + } + + string typeGuid = DiskPartOutputParser.ParseDetailPartitionTypeGuid(execution.StandardOutput); + if (typeGuid.Length > 0) + { + result[partition.Number] = typeGuid; + } + } + + return result; + } + /// public async Task ResolveImageIndexAsync( string imagePath, @@ -932,6 +1088,13 @@ private static char GetAvailableLetter(HashSet usedLetters, IReadOnlyList< throw new InvalidOperationException("No drive letter is available for deployment partitions."); } + private static bool IsPartitionType( + DiskPartPartition partition, + IReadOnlyDictionary partitionTypeGuids, + string expectedTypeGuid) + => partitionTypeGuids.TryGetValue(partition.Number, out string? typeGuid) && + typeGuid.Equals(expectedTypeGuid, StringComparison.OrdinalIgnoreCase); + private static void ResetWorkingDirectory(string path) { TryDeleteDirectory(path); @@ -1100,4 +1263,11 @@ private sealed record ImageIndexDescriptor public required string Edition { get; init; } public required string EditionId { get; init; } } + + private sealed record RecoveryDiskLayoutProbe + { + public int SystemPartitionNumber { get; init; } + public int RecoveryPartitionNumber { get; init; } + public int WindowsPartitionNumber { get; init; } + } } diff --git a/src/Foundry.Deploy/Services/Hardware/HardwareProfileService.cs b/src/Foundry.Deploy/Services/Hardware/HardwareProfileService.cs index 4751da8e..20a9b132 100644 --- a/src/Foundry.Deploy/Services/Hardware/HardwareProfileService.cs +++ b/src/Foundry.Deploy/Services/Hardware/HardwareProfileService.cs @@ -1,119 +1,41 @@ -using System.Text.Json; -using System.Text; -using System.IO; +using System.Text.RegularExpressions; using Foundry.Deploy.Models; -using Foundry.Deploy.Services.System; using Microsoft.Extensions.Logging; namespace Foundry.Deploy.Services.Hardware; -public sealed class HardwareProfileService : IHardwareProfileService +public sealed partial class HardwareProfileService : IHardwareProfileService { - private readonly IProcessRunner _processRunner; + private const string SystemFirmwareClassGuid = "{f2e7dd72-6468-4e36-b6f1-6488f42c1b52}"; + + private readonly IHardwareProfileSource _source; private readonly ILogger _logger; + private readonly Func _architectureProvider; - public HardwareProfileService(IProcessRunner processRunner, ILogger logger) + public HardwareProfileService(ILogger logger) + : this(new WindowsHardwareProfileSource(), logger, () => Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE") ?? string.Empty) { - _processRunner = processRunner; - _logger = logger; } - public async Task GetCurrentAsync(CancellationToken cancellationToken = default) + internal HardwareProfileService( + IHardwareProfileSource source, + ILogger logger, + Func architectureProvider) { - _logger.LogInformation("Detecting current hardware profile."); - string script = @" -function ConvertTo-TrimmedString { - param ( - [Parameter(ValueFromPipeline = $true)] - $Value - ) - - process { - if ($null -eq $Value) { - return '' - } - - return $Value.ToString().Trim() - } -} - -$computer = Get-CimInstance -ClassName Win32_ComputerSystem -$product = Get-CimInstance -ClassName Win32_ComputerSystemProduct -$bios = Get-CimInstance -ClassName Win32_BIOS -$tpm = Get-CimInstance -Namespace 'ROOT\cimv2\Security\MicrosoftTpm' -ClassName Win32_Tpm -ErrorAction SilentlyContinue -$battery = Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue -$pnpDevices = @(Get-CimInstance -ClassName Win32_PnpEntity -Property Name,DeviceID,HardwareID,ClassGuid,Manufacturer,PNPClass -ErrorAction SilentlyContinue | ForEach-Object { - $hardwareIds = @($_.HardwareID | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.ToString().Trim() }) - [pscustomobject]@{ - Name = [string]($_.Name | ConvertTo-TrimmedString) - DeviceId = [string]($_.DeviceID | ConvertTo-TrimmedString) - HardwareIds = $hardwareIds - ClassGuid = [string]($_.ClassGuid | ConvertTo-TrimmedString) - Manufacturer = [string]($_.Manufacturer | ConvertTo-TrimmedString) - PnpClass = [string]($_.PNPClass | ConvertTo-TrimmedString) + _source = source; + _logger = logger; + _architectureProvider = architectureProvider; } -}) -$firmwareDevice = $pnpDevices | Where-Object { $_.ClassGuid -eq '{f2e7dd72-6468-4e36-b6f1-6488f42c1b52}' } | Select-Object -First 1 -$systemFirmwareHardwareId = '' -if ($firmwareDevice -and $firmwareDevice.DeviceId -match '\{?(([0-9a-f]){8}-([0-9a-f]){4}-([0-9a-f]){4}-([0-9a-f]){4}-([0-9a-f]){12})\}?') { - $systemFirmwareHardwareId = $Matches[1] -} -$isOnBattery = @($battery | Where-Object { $_.BatteryStatus -eq 1 }).Count -gt 0 - -[pscustomobject]@{ - Manufacturer = [string]$computer.Manufacturer - Model = [string]$computer.Model - Product = [string]$product.Version - SerialNumber = [string]$bios.SerialNumber - Architecture = [string]$env:PROCESSOR_ARCHITECTURE - IsOnBattery = [bool]$isOnBattery - IsTpmPresent = [bool]($null -ne $tpm) - SystemFirmwareHardwareId = [string]$systemFirmwareHardwareId - PnpDevices = $pnpDevices -} | ConvertTo-Json -Compress -Depth 8 -"; - string encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(script)); - string args = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encoded}"; - ProcessExecutionResult execution = await _processRunner - .RunAsync("powershell.exe", args, Path.GetTempPath(), cancellationToken) - .ConfigureAwait(false); - - if (!execution.IsSuccess || string.IsNullOrWhiteSpace(execution.StandardOutput)) - { - _logger.LogWarning("Hardware profile detection returned no data. Using fallback profile. ExitCode={ExitCode}", execution.ExitCode); - return BuildFallbackProfile(); - } + public Task GetCurrentAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Detecting current hardware profile."); try { - using JsonDocument document = JsonDocument.Parse(execution.StandardOutput); - JsonElement root = document.RootElement; - - string manufacturer = ReadProperty(root, "Manufacturer"); - string model = ReadProperty(root, "Model"); - string product = ReadProperty(root, "Product"); - string serial = ReadProperty(root, "SerialNumber"); - string architecture = NormalizeArchitecture(ReadProperty(root, "Architecture")); - bool isVirtualMachine = IsVirtualMachine(manufacturer, model, product); - bool isOnBattery = ReadBoolProperty(root, "IsOnBattery"); - bool isTpmPresent = ReadBoolProperty(root, "IsTpmPresent"); - string systemFirmwareHardwareId = ReadProperty(root, "SystemFirmwareHardwareId"); - IReadOnlyList pnpDevices = ReadPnpDevices(root); - - HardwareProfile profile = new() - { - Manufacturer = NormalizeManufacturer(manufacturer), - Model = NormalizeValue(model), - Product = NormalizeValue(product), - SerialNumber = NormalizeValue(serial), - Architecture = architecture, - IsVirtualMachine = isVirtualMachine, - IsOnBattery = isOnBattery, - IsTpmPresent = isTpmPresent, - SystemFirmwareHardwareId = systemFirmwareHardwareId.Trim(), - PnpDevices = pnpDevices - }; + HardwareProfileSnapshot snapshot = _source.Capture(); + HardwareProfile profile = BuildProfile(snapshot); _logger.LogInformation("Hardware profile detected. Manufacturer={Manufacturer}, Model={Model}, Architecture={Architecture}, IsVirtualMachine={IsVirtualMachine}, IsOnBattery={IsOnBattery}, IsTpmPresent={IsTpmPresent}", profile.Manufacturer, @@ -122,25 +44,48 @@ function ConvertTo-TrimmedString { profile.IsVirtualMachine, profile.IsOnBattery, profile.IsTpmPresent); - return profile; + + return Task.FromResult(profile); } catch (Exception ex) { - _logger.LogError(ex, "Failed to parse hardware profile payload. Falling back to default profile."); - return BuildFallbackProfile(); + _logger.LogWarning(ex, "Native hardware profile detection failed. Using fallback hardware profile."); + return Task.FromResult(BuildFallbackProfile()); } } - private static HardwareProfile BuildFallbackProfile() + private HardwareProfile BuildProfile(HardwareProfileSnapshot snapshot) + { + string manufacturer = NormalizeManufacturer(snapshot.Manufacturer); + string model = NormalizeValue(snapshot.Model); + string product = NormalizeValue(snapshot.Product); + string serial = NormalizeValue(snapshot.SerialNumber); + IReadOnlyList devices = snapshot.Devices; + + return new HardwareProfile + { + Manufacturer = manufacturer, + Model = model, + Product = product, + SerialNumber = serial, + Architecture = NormalizeArchitecture(_architectureProvider()), + IsVirtualMachine = IsVirtualMachine(manufacturer, model, product), + IsOnBattery = snapshot.IsOnBattery, + IsTpmPresent = HasTpmDevice(devices), + SystemFirmwareHardwareId = ResolveSystemFirmwareHardwareId(devices), + PnpDevices = devices + }; + } + + private HardwareProfile BuildFallbackProfile() { - string architecture = NormalizeArchitecture(Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE") ?? string.Empty); return new HardwareProfile { Manufacturer = "Unknown", Model = "Unknown", Product = "Unknown", SerialNumber = "Unknown", - Architecture = architecture, + Architecture = NormalizeArchitecture(_architectureProvider()), IsVirtualMachine = false, IsOnBattery = false, IsTpmPresent = false, @@ -149,80 +94,33 @@ private static HardwareProfile BuildFallbackProfile() }; } - private static string ReadProperty(JsonElement root, string propertyName) + private static string ResolveSystemFirmwareHardwareId(IReadOnlyList devices) { - return root.TryGetProperty(propertyName, out JsonElement value) - ? value.GetString() ?? string.Empty - : string.Empty; - } + PnpDeviceInfo? firmwareDevice = devices.FirstOrDefault(device => + string.Equals(device.ClassGuid, SystemFirmwareClassGuid, StringComparison.OrdinalIgnoreCase)); - private static bool ReadBoolProperty(JsonElement root, string propertyName) - { - if (!root.TryGetProperty(propertyName, out JsonElement value)) + if (firmwareDevice is null) { - return false; + return string.Empty; } - return value.ValueKind == JsonValueKind.True || - (value.ValueKind == JsonValueKind.String && bool.TryParse(value.GetString(), out bool parsed) && parsed); - } - - private static IReadOnlyList ReadPnpDevices(JsonElement root) - { - if (!root.TryGetProperty("PnpDevices", out JsonElement devicesElement) || - devicesElement.ValueKind != JsonValueKind.Array) + Match match = FirmwareHardwareIdRegex().Match(firmwareDevice.DeviceId); + if (!match.Success) { - return Array.Empty(); + match = firmwareDevice.HardwareIds + .Select(id => FirmwareHardwareIdRegex().Match(id)) + .FirstOrDefault(candidate => candidate.Success) ?? Match.Empty; } - var devices = new List(); - foreach (JsonElement deviceElement in devicesElement.EnumerateArray()) - { - if (deviceElement.ValueKind != JsonValueKind.Object) - { - continue; - } - - devices.Add(new PnpDeviceInfo - { - Name = ReadProperty(deviceElement, "Name"), - DeviceId = ReadProperty(deviceElement, "DeviceId"), - HardwareIds = ReadStringArrayProperty(deviceElement, "HardwareIds"), - ClassGuid = ReadProperty(deviceElement, "ClassGuid"), - Manufacturer = ReadProperty(deviceElement, "Manufacturer"), - PnpClass = ReadProperty(deviceElement, "PnpClass") - }); - } - - return devices; + return match.Success ? match.Groups[1].Value : string.Empty; } - private static IReadOnlyList ReadStringArrayProperty(JsonElement root, string propertyName) + private static bool HasTpmDevice(IReadOnlyList devices) { - if (!root.TryGetProperty(propertyName, out JsonElement value)) - { - return Array.Empty(); - } - - if (value.ValueKind == JsonValueKind.Array) - { - return value - .EnumerateArray() - .Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() ?? string.Empty : string.Empty) - .Where(item => !string.IsNullOrWhiteSpace(item)) - .Select(item => item.Trim()) - .ToArray(); - } - - if (value.ValueKind == JsonValueKind.String) - { - string? stringValue = value.GetString(); - return string.IsNullOrWhiteSpace(stringValue) - ? Array.Empty() - : [stringValue.Trim()]; - } - - return Array.Empty(); + return devices.Any(device => + device.HardwareIds.Any(id => id.Contains("MSFT0101", StringComparison.OrdinalIgnoreCase)) || + device.Name.Contains("Trusted Platform Module", StringComparison.OrdinalIgnoreCase) || + device.PnpClass.Contains("SecurityDevices", StringComparison.OrdinalIgnoreCase)); } private static bool IsVirtualMachine(string manufacturer, string model, string product) @@ -290,4 +188,7 @@ private static string NormalizeValue(string value) string normalized = value.Trim(); return string.IsNullOrWhiteSpace(normalized) ? "Unknown" : normalized; } + + [GeneratedRegex(@"\{?(([0-9a-f]){8}-([0-9a-f]){4}-([0-9a-f]){4}-([0-9a-f]){4}-([0-9a-f]){12})\}?", RegexOptions.IgnoreCase)] + private static partial Regex FirmwareHardwareIdRegex(); } diff --git a/src/Foundry.Deploy/Services/Hardware/HardwareProfileSnapshot.cs b/src/Foundry.Deploy/Services/Hardware/HardwareProfileSnapshot.cs new file mode 100644 index 00000000..0ba59ef8 --- /dev/null +++ b/src/Foundry.Deploy/Services/Hardware/HardwareProfileSnapshot.cs @@ -0,0 +1,11 @@ +using Foundry.Deploy.Models; + +namespace Foundry.Deploy.Services.Hardware; + +internal sealed record HardwareProfileSnapshot( + string Manufacturer, + string Model, + string Product, + string SerialNumber, + bool IsOnBattery, + IReadOnlyList Devices); diff --git a/src/Foundry.Deploy/Services/Hardware/IHardwareProfileSource.cs b/src/Foundry.Deploy/Services/Hardware/IHardwareProfileSource.cs new file mode 100644 index 00000000..289a8e35 --- /dev/null +++ b/src/Foundry.Deploy/Services/Hardware/IHardwareProfileSource.cs @@ -0,0 +1,6 @@ +namespace Foundry.Deploy.Services.Hardware; + +internal interface IHardwareProfileSource +{ + HardwareProfileSnapshot Capture(); +} diff --git a/src/Foundry.Deploy/Services/Hardware/TargetDiskService.cs b/src/Foundry.Deploy/Services/Hardware/TargetDiskService.cs index d78f872e..0bf99634 100644 --- a/src/Foundry.Deploy/Services/Hardware/TargetDiskService.cs +++ b/src/Foundry.Deploy/Services/Hardware/TargetDiskService.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Text; -using System.Text.Json; using Foundry.Deploy.Models; using Foundry.Deploy.Services.Localization; using Foundry.Deploy.Services.System; @@ -22,32 +20,9 @@ public TargetDiskService(IProcessRunner processRunner, ILogger> GetDisksAsync(CancellationToken cancellationToken = default) { _logger.LogInformation("Querying target disks."); - string script = @" -$disks = Get-Disk | Sort-Object -Property Number -$result = foreach ($disk in $disks) { - [pscustomobject]@{ - Number = [int]$disk.Number - FriendlyName = [string]$disk.FriendlyName - SerialNumber = [string]$disk.SerialNumber - BusType = [string]$disk.BusType - PartitionStyle = [string]$disk.PartitionStyle - Size = [uint64]$disk.Size - IsSystem = [bool]$disk.IsSystem - IsBoot = [bool]$disk.IsBoot - IsReadOnly = [bool]$disk.IsReadOnly - IsOffline = [bool]$disk.IsOffline - IsRemovable = [bool]$disk.IsRemovable - } -} -$result | ConvertTo-Json -Compress -"; - - string encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(script)); - string args = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encoded}"; - - ProcessExecutionResult execution = await _processRunner - .RunAsync("powershell.exe", args, Path.GetTempPath(), cancellationToken) - .ConfigureAwait(false); + ProcessExecutionResult execution = await RunDiskPartScriptAsync( + ["list disk"], + cancellationToken).ConfigureAwait(false); if (!execution.IsSuccess || string.IsNullOrWhiteSpace(execution.StandardOutput)) { @@ -57,34 +32,20 @@ public async Task> GetDisksAsync(CancellationToken try { - using JsonDocument document = JsonDocument.Parse(execution.StandardOutput); - JsonElement root = document.RootElement; - var disks = new List(); - if (root.ValueKind == JsonValueKind.Array) + foreach (DiskPartDisk disk in DiskPartOutputParser.ParseListDisk(execution.StandardOutput)) { - foreach (JsonElement diskElement in root.EnumerateArray()) - { - TargetDiskInfo info = ParseDisk(diskElement); - if (ShouldExcludeFromTargets(info)) - { - _logger.LogInformation( - "Skipping disk {DiskNumber} from target selection because it is attached over USB. FriendlyName={FriendlyName}", - info.DiskNumber, - info.FriendlyName); - continue; - } - - disks.Add(info); - } - } - else if (root.ValueKind == JsonValueKind.Object) - { - TargetDiskInfo info = ParseDisk(root); + TargetDiskInfo info = await QueryDiskInfoAsync(disk, cancellationToken).ConfigureAwait(false); if (!ShouldExcludeFromTargets(info)) { disks.Add(info); + continue; } + + _logger.LogInformation( + "Skipping disk {DiskNumber} from target selection because it is attached over USB. FriendlyName={FriendlyName}", + info.DiskNumber, + info.FriendlyName); } TargetDiskInfo[] orderedDisks = disks @@ -123,23 +84,12 @@ public async Task> GetDisksAsync(CancellationToken return null; } - string script = $@" -$partition = Get-Partition -DriveLetter '{EscapeForSingleQuote(driveLetter)}' -ErrorAction SilentlyContinue -if ($null -eq $partition) {{ - return -}} - -[pscustomobject]@{{ - DiskNumber = [int]$partition.DiskNumber -}} | ConvertTo-Json -Compress -"; - - string encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(script)); - string args = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encoded}"; - - ProcessExecutionResult execution = await _processRunner - .RunAsync("powershell.exe", args, Path.GetTempPath(), cancellationToken) - .ConfigureAwait(false); + ProcessExecutionResult execution = await RunDiskPartScriptAsync( + [ + $"select volume {driveLetter}", + "detail volume" + ], + cancellationToken).ConfigureAwait(false); if (!execution.IsSuccess || string.IsNullOrWhiteSpace(execution.StandardOutput)) { @@ -147,63 +97,37 @@ public async Task> GetDisksAsync(CancellationToken return null; } - try - { - using JsonDocument document = JsonDocument.Parse(execution.StandardOutput); - JsonElement rootElement = document.RootElement; - if (!rootElement.TryGetProperty("DiskNumber", out JsonElement diskNumberElement)) - { - return null; - } - - if (diskNumberElement.ValueKind == JsonValueKind.Number && diskNumberElement.TryGetInt32(out int numericValue)) - { - return numericValue; - } - - if (diskNumberElement.ValueKind == JsonValueKind.String && - int.TryParse(diskNumberElement.GetString(), out int parsedValue)) - { - return parsedValue; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to parse disk number for path {Path}.", path); - return null; - } - - return null; + return DiskPartOutputParser.ParseDetailVolumeDiskNumber(execution.StandardOutput); } - private static TargetDiskInfo ParseDisk(JsonElement element) + private async Task QueryDiskInfoAsync(DiskPartDisk disk, CancellationToken cancellationToken) { - int diskNumber = ReadInt(element, "Number"); - string friendlyName = NormalizeValue(ReadString(element, "FriendlyName"), fallback: LocalizationText.GetString("Common.Unknown")); - string serial = NormalizeValue(ReadString(element, "SerialNumber"), fallback: LocalizationText.GetString("Common.Unknown")); - string busType = NormalizeValue(ReadString(element, "BusType"), fallback: LocalizationText.GetString("Common.Unknown")); - string partitionStyle = NormalizeValue(ReadString(element, "PartitionStyle"), fallback: LocalizationText.GetString("Common.Unknown")); - ulong sizeBytes = ReadUInt64(element, "Size"); - bool isSystem = ReadBool(element, "IsSystem"); - bool isBoot = ReadBool(element, "IsBoot"); - bool isReadOnly = ReadBool(element, "IsReadOnly"); - bool isOffline = ReadBool(element, "IsOffline"); - bool isRemovable = ReadBool(element, "IsRemovable"); + ProcessExecutionResult execution = await RunDiskPartScriptAsync( + [ + $"select disk {disk.Number}", + "detail disk" + ], + cancellationToken).ConfigureAwait(false); + + DiskPartDetailDisk detail = execution.IsSuccess && !string.IsNullOrWhiteSpace(execution.StandardOutput) + ? DiskPartOutputParser.ParseDetailDisk(disk.Number, execution.StandardOutput, disk) + : new DiskPartDetailDisk(disk.Number, string.Empty, string.Empty, string.Empty, disk.IsGpt ? "GPT" : "MBR", disk.SizeBytes, false, false, false, disk.IsOffline); - string warning = BuildSelectionWarning(isSystem, isBoot, isReadOnly, isOffline); + bool isRemovable = string.Equals(detail.BusType, "USB", StringComparison.OrdinalIgnoreCase); + string warning = BuildSelectionWarning(detail.IsSystem, detail.IsBoot, detail.IsReadOnly, detail.IsOffline); return new TargetDiskInfo { - DiskNumber = diskNumber, - FriendlyName = friendlyName, - SerialNumber = serial, - BusType = busType, - PartitionStyle = partitionStyle, - SizeBytes = sizeBytes, - IsSystem = isSystem, - IsBoot = isBoot, - IsReadOnly = isReadOnly, - IsOffline = isOffline, + DiskNumber = detail.Number, + FriendlyName = NormalizeValue(detail.FriendlyName, fallback: LocalizationText.GetString("Common.Unknown")), + SerialNumber = NormalizeValue(detail.SerialNumber, fallback: LocalizationText.GetString("Common.Unknown")), + BusType = NormalizeValue(detail.BusType, fallback: LocalizationText.GetString("Common.Unknown")), + PartitionStyle = NormalizeValue(detail.PartitionStyle, fallback: LocalizationText.GetString("Common.Unknown")), + SizeBytes = detail.SizeBytes, + IsSystem = detail.IsSystem, + IsBoot = detail.IsBoot, + IsReadOnly = detail.IsReadOnly, + IsOffline = detail.IsOffline, IsRemovable = isRemovable, IsSelectable = string.IsNullOrWhiteSpace(warning), SelectionWarning = warning @@ -238,91 +162,41 @@ private static string BuildSelectionWarning(bool isSystem, bool isBoot, bool isR private static bool ShouldExcludeFromTargets(TargetDiskInfo disk) => string.Equals(disk.BusType, "USB", StringComparison.OrdinalIgnoreCase); - private static string ReadString(JsonElement root, string propertyName) - { - if (!root.TryGetProperty(propertyName, out JsonElement property)) - { - return string.Empty; - } - - return property.ValueKind == JsonValueKind.String - ? property.GetString() ?? string.Empty - : property.ToString(); - } - - private static int ReadInt(JsonElement root, string propertyName) + private static string NormalizeValue(string value, string fallback) { - if (!root.TryGetProperty(propertyName, out JsonElement property)) - { - return -1; - } - - if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out int value)) - { - return value; - } - - if (property.ValueKind == JsonValueKind.String && int.TryParse(property.GetString(), out int parsed)) - { - return parsed; - } - - return -1; + string normalized = value.Trim(); + return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized; } - private static ulong ReadUInt64(JsonElement root, string propertyName) + private async Task RunDiskPartScriptAsync( + IReadOnlyList scriptLines, + CancellationToken cancellationToken) { - if (!root.TryGetProperty(propertyName, out JsonElement property)) - { - return 0; - } - - if (property.ValueKind == JsonValueKind.Number && property.TryGetUInt64(out ulong value)) + string scriptPath = Path.Combine(Path.GetTempPath(), $"foundry-diskpart-{Guid.NewGuid():N}.txt"); + try { - return value; + await File.WriteAllLinesAsync(scriptPath, scriptLines, cancellationToken).ConfigureAwait(false); + return await _processRunner + .RunAsync("diskpart.exe", $"/s \"{scriptPath}\"", Path.GetTempPath(), cancellationToken) + .ConfigureAwait(false); } - - if (property.ValueKind == JsonValueKind.String && ulong.TryParse(property.GetString(), out ulong parsed)) + finally { - return parsed; + TryDeleteFile(scriptPath); } - - return 0; } - private static bool ReadBool(JsonElement root, string propertyName) + private static void TryDeleteFile(string path) { - if (!root.TryGetProperty(propertyName, out JsonElement property)) - { - return false; - } - - if (property.ValueKind == JsonValueKind.True) - { - return true; - } - - if (property.ValueKind == JsonValueKind.False) + try { - return false; + if (File.Exists(path)) + { + File.Delete(path); + } } - - if (property.ValueKind == JsonValueKind.String && bool.TryParse(property.GetString(), out bool parsed)) + catch { - return parsed; } - - return false; - } - - private static string NormalizeValue(string value, string fallback) - { - string normalized = value.Trim(); - return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized; - } - - private static string EscapeForSingleQuote(string value) - { - return value.Replace("'", "''", StringComparison.Ordinal); } } diff --git a/src/Foundry.Deploy/Services/Hardware/WindowsHardwareProfileSource.cs b/src/Foundry.Deploy/Services/Hardware/WindowsHardwareProfileSource.cs new file mode 100644 index 00000000..daa49217 --- /dev/null +++ b/src/Foundry.Deploy/Services/Hardware/WindowsHardwareProfileSource.cs @@ -0,0 +1,211 @@ +using System.Runtime.InteropServices; +using System.Text; +using Foundry.Deploy.Models; +using Microsoft.Win32; + +namespace Foundry.Deploy.Services.Hardware; + +internal sealed class WindowsHardwareProfileSource : IHardwareProfileSource +{ + private const string BiosRegistryPath = @"HARDWARE\DESCRIPTION\System\BIOS"; + private const uint DigcfPresent = 0x00000002; + private const uint DigcfAllClasses = 0x00000004; + private const uint SpdrpDevicedesc = 0x00000000; + private const uint SpdrpHardwareId = 0x00000001; + private const uint SpdrpMfg = 0x0000000B; + private const uint SpdrpClass = 0x00000007; + private const uint SpdrpClassGuid = 0x00000008; + private const uint SpdrpFriendlyName = 0x0000000C; + private static readonly IntPtr InvalidHandleValue = new(-1); + + public HardwareProfileSnapshot Capture() + { + IReadOnlyList devices = EnumeratePnpDevices(); + + return new HardwareProfileSnapshot( + Manufacturer: ReadBiosValue("SystemManufacturer"), + Model: ReadBiosValue("SystemProductName"), + Product: ReadBiosValue("SystemVersion"), + SerialNumber: ReadBiosValue("SystemSerialNumber"), + IsOnBattery: IsSystemOnBattery(), + Devices: devices); + } + + private static string ReadBiosValue(string name) + { + try + { + using RegistryKey? key = Registry.LocalMachine.OpenSubKey(BiosRegistryPath); + return key?.GetValue(name)?.ToString() ?? string.Empty; + } + catch + { + return string.Empty; + } + } + + private static bool IsSystemOnBattery() + { + return GetSystemPowerStatus(out SystemPowerStatus status) && status.ACLineStatus == 0; + } + + private static IReadOnlyList EnumeratePnpDevices() + { + IntPtr deviceInfoSet = SetupDiGetClassDevs(IntPtr.Zero, null, IntPtr.Zero, DigcfPresent | DigcfAllClasses); + if (deviceInfoSet == InvalidHandleValue) + { + return []; + } + + try + { + var devices = new List(); + for (uint index = 0; ; index++) + { + SpDevinfoData deviceInfoData = new() + { + CbSize = (uint)Marshal.SizeOf() + }; + + if (!SetupDiEnumDeviceInfo(deviceInfoSet, index, ref deviceInfoData)) + { + break; + } + + string friendlyName = ReadDeviceProperty(deviceInfoSet, ref deviceInfoData, SpdrpFriendlyName); + string description = ReadDeviceProperty(deviceInfoSet, ref deviceInfoData, SpdrpDevicedesc); + devices.Add(new PnpDeviceInfo + { + Name = FirstNonEmpty(friendlyName, description), + DeviceId = ReadDeviceInstanceId(deviceInfoSet, ref deviceInfoData), + HardwareIds = ReadDeviceMultiStringProperty(deviceInfoSet, ref deviceInfoData, SpdrpHardwareId), + ClassGuid = ReadDeviceProperty(deviceInfoSet, ref deviceInfoData, SpdrpClassGuid), + Manufacturer = ReadDeviceProperty(deviceInfoSet, ref deviceInfoData, SpdrpMfg), + PnpClass = ReadDeviceProperty(deviceInfoSet, ref deviceInfoData, SpdrpClass) + }); + } + + return devices; + } + finally + { + SetupDiDestroyDeviceInfoList(deviceInfoSet); + } + } + + private static string ReadDeviceProperty(IntPtr deviceInfoSet, ref SpDevinfoData deviceInfoData, uint property) + { + IReadOnlyList values = ReadDeviceMultiStringProperty(deviceInfoSet, ref deviceInfoData, property); + return values.Count > 0 ? values[0] : string.Empty; + } + + private static IReadOnlyList ReadDeviceMultiStringProperty(IntPtr deviceInfoSet, ref SpDevinfoData deviceInfoData, uint property) + { + byte[] buffer = new byte[8192]; + return SetupDiGetDeviceRegistryProperty( + deviceInfoSet, + ref deviceInfoData, + property, + out _, + buffer, + (uint)buffer.Length, + out uint requiredSize) + ? DecodeRegistryString(buffer, requiredSize) + : []; + } + + private static string ReadDeviceInstanceId(IntPtr deviceInfoSet, ref SpDevinfoData deviceInfoData) + { + var buffer = new char[1024]; + return SetupDiGetDeviceInstanceId( + deviceInfoSet, + ref deviceInfoData, + buffer, + (uint)buffer.Length, + out _) + ? new string(buffer).TrimEnd('\0') + : string.Empty; + } + + private static IReadOnlyList DecodeRegistryString(byte[] buffer, uint requiredSize) + { + if (requiredSize == 0) + { + return []; + } + + int byteCount = Math.Min((int)requiredSize, buffer.Length); + string raw = Encoding.Unicode.GetString(buffer, 0, byteCount).TrimEnd('\0'); + if (string.IsNullOrWhiteSpace(raw)) + { + return []; + } + + return raw + .Split('\0', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .ToArray(); + } + + private static string FirstNonEmpty(params string[] values) + { + return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetSystemPowerStatus(out SystemPowerStatus systemPowerStatus); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr SetupDiGetClassDevs( + IntPtr classGuid, + string? enumerator, + IntPtr hwndParent, + uint flags); + + [DllImport("setupapi.dll", SetLastError = true)] + private static extern bool SetupDiEnumDeviceInfo( + IntPtr deviceInfoSet, + uint memberIndex, + ref SpDevinfoData deviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetupDiGetDeviceRegistryProperty( + IntPtr deviceInfoSet, + ref SpDevinfoData deviceInfoData, + uint property, + out uint propertyRegDataType, + byte[] propertyBuffer, + uint propertyBufferSize, + out uint requiredSize); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetupDiGetDeviceInstanceId( + IntPtr deviceInfoSet, + ref SpDevinfoData deviceInfoData, + char[] deviceInstanceId, + uint deviceInstanceIdSize, + out uint requiredSize); + + [DllImport("setupapi.dll", SetLastError = true)] + private static extern bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet); + + [StructLayout(LayoutKind.Sequential)] + private struct SystemPowerStatus + { + public byte ACLineStatus; + public byte BatteryFlag; + public byte BatteryLifePercent; + public byte SystemStatusFlag; + public int BatteryLifeTime; + public int BatteryFullLifeTime; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SpDevinfoData + { + public uint CbSize; + public Guid ClassGuid; + public uint DevInst; + public IntPtr Reserved; + } +} diff --git a/src/Foundry.Deploy/Services/Localization/DeploymentUiTextLocalizer.cs b/src/Foundry.Deploy/Services/Localization/DeploymentUiTextLocalizer.cs index 3a87aa88..715c2ff8 100644 --- a/src/Foundry.Deploy/Services/Localization/DeploymentUiTextLocalizer.cs +++ b/src/Foundry.Deploy/Services/Localization/DeploymentUiTextLocalizer.cs @@ -30,6 +30,7 @@ public static string LocalizeStepName(string value) "Extract driver pack" => LocalizationText.GetString("Step.ExtractDriverPack"), "Stage pre-OOBE customization" => LocalizationText.GetString("Step.StagePreOobeCustomization"), "Apply driver pack" => LocalizationText.GetString("Step.ApplyDriverPack"), + "Provision OS Recovery" => LocalizationText.GetString("Step.ProvisionOsRecovery"), "Download firmware update" => LocalizationText.GetString("Step.DownloadFirmwareUpdate"), "Apply firmware update" => LocalizationText.GetString("Step.ApplyFirmwareUpdate"), "Seal recovery partition" => LocalizationText.GetString("Step.SealRecoveryPartition"), @@ -170,6 +171,14 @@ public static string LocalizeMessage(string value) "Driver pack applied." => LocalizationText.GetString("StepResult.DriverPackApplied"), "Driver pack staged for first boot." => LocalizationText.GetString("StepResult.DriverPackStagedForFirstBoot"), "Driver pack applied (simulation)." => LocalizationText.GetString("StepResult.DriverPackAppliedSimulation"), + "OS Recovery is disabled." => LocalizationText.GetString("StepResult.OsRecoveryDisabled"), + "OS Recovery provisioning is skipped in recovery mode." => LocalizationText.GetString("StepResult.OsRecoverySkippedInRecoveryMode"), + "Provisioning OS Recovery..." => LocalizationText.GetString("StepMessage.ProvisioningOsRecovery"), + "Injecting OS Recovery payload..." => LocalizationText.GetString("StepMessage.InjectingOsRecoveryPayload"), + "Committing WinRE changes..." => LocalizationText.GetString("StepMessage.CommittingWinReChanges"), + "Registering OS Recovery boot menu..." => LocalizationText.GetString("StepMessage.RegisteringOsRecoveryBootMenu"), + "OS Recovery provisioned." => LocalizationText.GetString("StepResult.OsRecoveryProvisioned"), + "OS Recovery provisioned (simulation)." => LocalizationText.GetString("StepResult.OsRecoveryProvisionedSimulation"), "Downloading firmware update..." => LocalizationText.GetString("StepMessage.DownloadingFirmwareUpdate"), "Preparing Microsoft Update Catalog lookup..." => LocalizationText.GetString("StepMessage.PreparingMicrosoftUpdateCatalogLookup"), "Firmware updates are disabled." => LocalizationText.GetString("StepResult.FirmwareUpdatesDisabled"), diff --git a/src/Foundry.Deploy/Services/Runtime/DeploymentRuntimeContextService.cs b/src/Foundry.Deploy/Services/Runtime/DeploymentRuntimeContextService.cs index dd44bef5..20d00706 100644 --- a/src/Foundry.Deploy/Services/Runtime/DeploymentRuntimeContextService.cs +++ b/src/Foundry.Deploy/Services/Runtime/DeploymentRuntimeContextService.cs @@ -39,10 +39,11 @@ private static bool TryResolveDeploymentModeFromEnvironment(out DeploymentMode m { "usb" => DeploymentMode.Usb, "iso" => DeploymentMode.Iso, + "recovery" => DeploymentMode.Recovery, _ => default }; - return normalized is "usb" or "iso"; + return normalized is "usb" or "iso" or "recovery"; } private static string? TryGetUsbCacheRuntimeRoot() diff --git a/src/Foundry.Deploy/Services/Runtime/IRecoveryTargetDiskResolver.cs b/src/Foundry.Deploy/Services/Runtime/IRecoveryTargetDiskResolver.cs new file mode 100644 index 00000000..3eb72e99 --- /dev/null +++ b/src/Foundry.Deploy/Services/Runtime/IRecoveryTargetDiskResolver.cs @@ -0,0 +1,6 @@ +namespace Foundry.Deploy.Services.Runtime; + +public interface IRecoveryTargetDiskResolver +{ + Task ResolveAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Foundry.Deploy/Services/Runtime/RecoveryTargetDiskResolver.cs b/src/Foundry.Deploy/Services/Runtime/RecoveryTargetDiskResolver.cs new file mode 100644 index 00000000..8731aabd --- /dev/null +++ b/src/Foundry.Deploy/Services/Runtime/RecoveryTargetDiskResolver.cs @@ -0,0 +1,189 @@ +using System.IO; +using Foundry.Deploy.Services.System; +using Microsoft.Extensions.Logging; + +namespace Foundry.Deploy.Services.Runtime; + +public sealed class RecoveryTargetDiskResolver : IRecoveryTargetDiskResolver +{ + private const string RecoveryPartitionGuid = "de94bba4-06d1-4d40-a16a-bfd50179d6ac"; + private const string RecoveryMarkerRelativePath = @"Recovery\WindowsRE\FoundryOsRecovery.json"; + private readonly IProcessRunner _processRunner; + private readonly ILogger _logger; + private readonly Func _fileExists; + + public RecoveryTargetDiskResolver(IProcessRunner processRunner, ILogger logger) + : this(processRunner, logger, File.Exists) + { + } + + internal RecoveryTargetDiskResolver( + IProcessRunner processRunner, + ILogger logger, + Func fileExists) + { + _processRunner = processRunner; + _logger = logger; + _fileExists = fileExists; + } + + public async Task ResolveAsync(CancellationToken cancellationToken = default) + { + ProcessExecutionResult listExecution = await RunDiskPartScriptAsync( + ["list disk"], + cancellationToken).ConfigureAwait(false); + + if (!listExecution.IsSuccess || string.IsNullOrWhiteSpace(listExecution.StandardOutput)) + { + _logger.LogWarning("Unable to resolve active OS Recovery target disk. ExitCode={ExitCode}", listExecution.ExitCode); + return null; + } + + var candidateDiskNumbers = new List(); + HashSet usedLetters = DriveInfo.GetDrives() + .Select(drive => char.ToUpperInvariant(drive.Name[0])) + .ToHashSet(); + + foreach (DiskPartDisk disk in DiskPartOutputParser.ParseListDisk(listExecution.StandardOutput)) + { + ProcessExecutionResult partitionExecution = await RunDiskPartScriptAsync( + [ + $"select disk {disk.Number}", + "list partition" + ], + cancellationToken).ConfigureAwait(false); + + if (!partitionExecution.IsSuccess || string.IsNullOrWhiteSpace(partitionExecution.StandardOutput)) + { + continue; + } + + foreach (DiskPartPartition partition in DiskPartOutputParser.ParseListPartition(partitionExecution.StandardOutput)) + { + ProcessExecutionResult detailExecution = await RunDiskPartScriptAsync( + [ + $"select disk {disk.Number}", + $"select partition {partition.Number}", + "detail partition" + ], + cancellationToken).ConfigureAwait(false); + + if (!detailExecution.IsSuccess || string.IsNullOrWhiteSpace(detailExecution.StandardOutput)) + { + continue; + } + + if (!DiskPartOutputParser + .ParseDetailPartitionTypeGuid(detailExecution.StandardOutput) + .Equals(RecoveryPartitionGuid, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + char? existingLetter = DiskPartOutputParser.ParseDetailPartitionDriveLetter(detailExecution.StandardOutput); + if (existingLetter.HasValue && + _fileExists($@"{existingLetter.Value}:\{RecoveryMarkerRelativePath}")) + { + candidateDiskNumbers.Add(disk.Number); + continue; + } + + char? letter = GetAvailableTemporaryLetter(usedLetters); + if (letter is null) + { + _logger.LogWarning("No temporary drive letter is available to inspect OS Recovery partition markers."); + return null; + } + + usedLetters.Add(letter.Value); + string markerPath = $@"{letter.Value}:\{RecoveryMarkerRelativePath}"; + bool assigned = false; + try + { + ProcessExecutionResult assignExecution = await RunDiskPartScriptAsync( + [ + $"select disk {disk.Number}", + $"select partition {partition.Number}", + $"assign letter={letter}" + ], + cancellationToken).ConfigureAwait(false); + + assigned = assignExecution.IsSuccess; + if (assigned && _fileExists(markerPath)) + { + candidateDiskNumbers.Add(disk.Number); + } + } + finally + { + if (assigned) + { + await RunDiskPartScriptAsync( + [ + $"select volume {letter}", + $"remove letter={letter} noerr" + ], + CancellationToken.None).ConfigureAwait(false); + } + + usedLetters.Remove(letter.Value); + } + } + } + + if (candidateDiskNumbers.Count != 1) + { + _logger.LogWarning( + "OS Recovery target disk resolver found {CandidateCount} Foundry recovery partition marker(s).", + candidateDiskNumbers.Count); + return null; + } + + return candidateDiskNumbers[0]; + } + + private async Task RunDiskPartScriptAsync( + IReadOnlyList scriptLines, + CancellationToken cancellationToken) + { + string scriptPath = Path.Combine(Path.GetTempPath(), $"foundry-recovery-diskpart-{Guid.NewGuid():N}.txt"); + try + { + await File.WriteAllLinesAsync(scriptPath, scriptLines, cancellationToken).ConfigureAwait(false); + return await _processRunner + .RunAsync("diskpart.exe", $"/s \"{scriptPath}\"", Path.GetTempPath(), cancellationToken) + .ConfigureAwait(false); + } + finally + { + TryDeleteFile(scriptPath); + } + } + + private static char? GetAvailableTemporaryLetter(HashSet usedLetters) + { + for (char letter = 'Z'; letter >= 'D'; letter--) + { + if (!usedLetters.Contains(letter)) + { + return letter; + } + } + + return null; + } + + private static void TryDeleteFile(string path) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + } + } +} diff --git a/src/Foundry.Deploy/Services/Startup/DeploymentStartupCoordinator.cs b/src/Foundry.Deploy/Services/Startup/DeploymentStartupCoordinator.cs index 0e5ed8fa..9381bd7c 100644 --- a/src/Foundry.Deploy/Services/Startup/DeploymentStartupCoordinator.cs +++ b/src/Foundry.Deploy/Services/Startup/DeploymentStartupCoordinator.cs @@ -24,6 +24,7 @@ public sealed class DeploymentStartupCoordinator : IDeploymentStartupCoordinator private readonly ITargetDiskService _targetDiskService; private readonly IDeploymentCatalogLoadService _deploymentCatalogLoadService; private readonly IAutopilotGroupTagDiscoveryService _autopilotGroupTagDiscoveryService; + private readonly IRecoveryTargetDiskResolver _recoveryTargetDiskResolver; private readonly ILogger _logger; public DeploymentStartupCoordinator( @@ -34,6 +35,7 @@ public DeploymentStartupCoordinator( ITargetDiskService targetDiskService, IDeploymentCatalogLoadService deploymentCatalogLoadService, IAutopilotGroupTagDiscoveryService autopilotGroupTagDiscoveryService, + IRecoveryTargetDiskResolver recoveryTargetDiskResolver, ILogger logger) { _deployConfigurationService = deployConfigurationService; @@ -43,6 +45,7 @@ public DeploymentStartupCoordinator( _targetDiskService = targetDiskService; _deploymentCatalogLoadService = deploymentCatalogLoadService; _autopilotGroupTagDiscoveryService = autopilotGroupTagDiscoveryService; + _recoveryTargetDiskResolver = recoveryTargetDiskResolver; _logger = logger; } @@ -59,7 +62,7 @@ public async Task InitializeAsync(DeploymentStartupRe Task computerNameTask = ResolveComputerNameAsync(request.FallbackComputerName); Task hardwareTask = LoadHardwareAsync(); - Task targetDisksTask = LoadTargetDisksAsync(); + Task targetDisksTask = LoadTargetDisksAsync(request.RuntimeContext.Mode); Task catalogTask = _deploymentCatalogLoadService.LoadAsync(); await Task.WhenAll(computerNameTask, hardwareTask, targetDisksTask, catalogTask).ConfigureAwait(false); @@ -112,12 +115,38 @@ private async Task LoadHardwareAsync() } } - private async Task LoadTargetDisksAsync() + private async Task LoadTargetDisksAsync(DeploymentMode mode) { try { IReadOnlyList disks = await _targetDiskService.GetDisksAsync().ConfigureAwait(false); - return new TargetDiskLoadResult(disks); + if (mode != DeploymentMode.Recovery) + { + return new TargetDiskLoadResult(disks); + } + + int? recoveryDiskNumber = await _recoveryTargetDiskResolver.ResolveAsync().ConfigureAwait(false); + if (!recoveryDiskNumber.HasValue) + { + _logger.LogWarning("OS Recovery mode could not resolve the active recovery target disk."); + return new TargetDiskLoadResult([]); + } + + TargetDiskInfo? recoveryDisk = disks.FirstOrDefault(disk => disk.DiskNumber == recoveryDiskNumber.Value); + if (recoveryDisk is null) + { + _logger.LogWarning("OS Recovery target disk {DiskNumber} was not returned by disk discovery.", recoveryDiskNumber.Value); + return new TargetDiskLoadResult([]); + } + + return new TargetDiskLoadResult( + [ + recoveryDisk with + { + IsSelectable = true, + SelectionWarning = string.Empty + } + ]); } catch (Exception ex) { diff --git a/src/Foundry.Deploy/Services/System/DiskPartOutputParser.cs b/src/Foundry.Deploy/Services/System/DiskPartOutputParser.cs new file mode 100644 index 00000000..d6c8fb48 --- /dev/null +++ b/src/Foundry.Deploy/Services/System/DiskPartOutputParser.cs @@ -0,0 +1,318 @@ +using System.Text.RegularExpressions; + +namespace Foundry.Deploy.Services.System; + +internal static class DiskPartOutputParser +{ + public static IReadOnlyList ParseListDisk(string output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return []; + } + + var disks = new List(); + foreach (string line in SplitLines(output)) + { + Match match = Regex.Match(line, @"^\s*\*?\s*\D+?(?\d+)\s+(?[^\s-]+)", RegexOptions.IgnoreCase); + if (!match.Success) + { + continue; + } + + disks.Add(new DiskPartDisk( + int.Parse(match.Groups["number"].Value), + ParseFirstSizeBytes(line), + line.TrimEnd().EndsWith('*'), + IsOfflineStatus(match.Groups["status"].Value))); + } + + return disks; + } + + public static IReadOnlyList ParseListPartition(string output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return []; + } + + var partitions = new List(); + foreach (string line in SplitLines(output)) + { + Match match = Regex.Match(line, @"^\s*\D+?(?\d+)\s+(?[^\s-]+)", RegexOptions.IgnoreCase); + + if (!match.Success) + { + continue; + } + + partitions.Add(new DiskPartPartition( + int.Parse(match.Groups["number"].Value), + match.Groups["type"].Value)); + } + + return partitions; + } + + public static DiskPartDetailDisk ParseDetailDisk(int diskNumber, string output, DiskPartDisk disk) + { + string friendlyName = string.Empty; + string serialNumber = string.Empty; + string busType = string.Empty; + string friendlyNameCandidate = string.Empty; + var busInferenceLines = new List(); + bool isBoot = false; + bool isSystem = false; + bool isReadOnly = false; + bool isOffline = disk.IsOffline; + + foreach (string rawLine in SplitLines(output)) + { + string line = rawLine.Trim(); + if (line.Length == 0) + { + continue; + } + + if (line.Contains(':', StringComparison.Ordinal) && friendlyName.Length == 0) + { + friendlyName = friendlyNameCandidate; + } + + if (!line.Contains(':', StringComparison.Ordinal) && !IsDiskPartBoilerplateLine(line)) + { + friendlyNameCandidate = line; + continue; + } + + if (IsBusInferenceLine(line)) + { + busInferenceLines.Add(line); + } + + if (TryReadKeyValue(line, @".*serial.*|.*s.rie.*", out string serial)) + { + serialNumber = serial; + continue; + } + + if (TryReadKeyValue(line, @"type", out string type)) + { + busType = type; + continue; + } + + if (TryReadKeyValue(line, @"status|statut", out string status)) + { + isOffline = IsOfflineStatus(status); + continue; + } + + if (TryReadKeyValue(line, @".*read-only.*|.*lecture\s+seule.*", out string currentReadOnly)) + { + isReadOnly = IsYes(currentReadOnly); + continue; + } + + if (TryReadKeyValue(line, @".*boot\s+disk.*|.*d.marrage.*", out string bootDisk)) + { + isBoot = IsYes(bootDisk); + continue; + } + + if (TryReadKeyValue(line, @".*system\s+disk.*|.*syst.me.*", out string systemDisk)) + { + isSystem = IsYes(systemDisk); + } + } + + if (string.IsNullOrWhiteSpace(busType)) + { + busType = InferBusType(friendlyName, busInferenceLines); + } + + return new DiskPartDetailDisk( + diskNumber, + friendlyName, + serialNumber, + busType, + disk.IsGpt ? "GPT" : "MBR", + disk.SizeBytes, + isSystem, + isBoot, + isReadOnly, + isOffline); + } + + public static int? ParseDetailVolumeDiskNumber(string output) + { + foreach (string line in SplitLines(output)) + { + Match match = Regex.Match(line, @"^\s*\*?\s*(?:Disk|Disque)\s+(?\d+)\b", RegexOptions.IgnoreCase); + if (match.Success) + { + return int.Parse(match.Groups["number"].Value); + } + } + + return null; + } + + public static string ParseDetailPartitionTypeGuid(string output) + { + Match match = Regex.Match( + output, + @"(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", + RegexOptions.IgnoreCase); + + return match.Success + ? match.Groups["guid"].Value.ToLowerInvariant() + : string.Empty; + } + + public static char? ParseDetailPartitionDriveLetter(string output) + { + foreach (string line in SplitLines(output)) + { + Match match = Regex.Match(line, @"^\s*(?:Volume)\s+\d+\s+(?[A-Z])\b", RegexOptions.IgnoreCase); + if (match.Success) + { + return char.ToUpperInvariant(match.Groups["letter"].Value[0]); + } + } + + return null; + } + + private static IReadOnlyList SplitLines(string output) + => output.Split(["\r\n", "\n"], StringSplitOptions.None); + + private static ulong ParseFirstSizeBytes(string line) + { + Match match = Regex.Match( + line, + @"(?\d+)\s*(?KB|MB|GB|TB|K|M|G|T|B|octets)(?:\s*octets)?", + RegexOptions.IgnoreCase); + if (!match.Success) + { + return 0; + } + + ulong value = ulong.Parse(match.Groups["value"].Value); + return match.Groups["unit"].Value.ToUpperInvariant() switch + { + "B" or "OCTETS" => value, + "K" or "KB" => value * 1024UL, + "M" or "MB" => value * 1024UL * 1024UL, + "G" or "GB" => value * 1024UL * 1024UL * 1024UL, + "T" or "TB" => value * 1024UL * 1024UL * 1024UL * 1024UL, + _ => 0 + }; + } + + private static bool TryReadKeyValue(string line, string keyPattern, out string value) + { + value = string.Empty; + Match match = Regex.Match(line, $"^(?:{keyPattern})\\s*:\\s*(?.*)$", RegexOptions.IgnoreCase); + if (!match.Success) + { + return false; + } + + value = match.Groups["value"].Value.Trim(); + return true; + } + + private static bool IsDiskPartBoilerplateLine(string line) + { + return line.StartsWith("Microsoft DiskPart", StringComparison.OrdinalIgnoreCase) || + line.StartsWith("Copyright", StringComparison.OrdinalIgnoreCase) || + line.StartsWith("DISKPART>", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsBusInferenceLine(string line) + { + if (!line.Contains(':', StringComparison.Ordinal)) + { + return false; + } + + return ResolveBusTypeFromText(line).Length > 0; + } + + private static string InferBusType(string friendlyName, IReadOnlyList inferenceLines) + { + return ResolveBusTypeFromText(string.Join('\n', inferenceLines.Prepend(friendlyName))); + } + + private static string ResolveBusTypeFromText(string value) + { + string combined = value.ToUpperInvariant(); + if (combined.Contains("NVME", StringComparison.Ordinal)) + { + return "NVMe"; + } + + if (combined.Contains("USB", StringComparison.Ordinal)) + { + return "USB"; + } + + if (combined.Contains("SATA", StringComparison.Ordinal)) + { + return "SATA"; + } + + if (combined.Contains("SCSI", StringComparison.Ordinal)) + { + return "SCSI"; + } + + if (combined.Contains("RAID", StringComparison.Ordinal)) + { + return "RAID"; + } + + if (combined.Contains("SAS", StringComparison.Ordinal)) + { + return "SAS"; + } + + if (combined.Contains("IDE", StringComparison.Ordinal)) + { + return "IDE"; + } + + if (combined.Contains("EMMC", StringComparison.Ordinal)) + { + return "eMMC"; + } + + return string.Empty; + } + + private static bool IsYes(string value) + => value.Equals("Yes", StringComparison.OrdinalIgnoreCase) || + value.Equals("Oui", StringComparison.OrdinalIgnoreCase); + + private static bool IsOfflineStatus(string value) + => value.Contains("offline", StringComparison.OrdinalIgnoreCase) || + value.Contains("hors", StringComparison.OrdinalIgnoreCase); +} + +internal sealed record DiskPartDisk(int Number, ulong SizeBytes, bool IsGpt, bool IsOffline); + +internal sealed record DiskPartPartition(int Number, string Type); + +internal sealed record DiskPartDetailDisk( + int Number, + string FriendlyName, + string SerialNumber, + string BusType, + string PartitionStyle, + ulong SizeBytes, + bool IsSystem, + bool IsBoot, + bool IsReadOnly, + bool IsOffline); diff --git a/src/Foundry.Deploy/Services/Wizard/DeploymentWizardContext.cs b/src/Foundry.Deploy/Services/Wizard/DeploymentWizardContext.cs index f5695330..d1c4bd9e 100644 --- a/src/Foundry.Deploy/Services/Wizard/DeploymentWizardContext.cs +++ b/src/Foundry.Deploy/Services/Wizard/DeploymentWizardContext.cs @@ -31,6 +31,7 @@ public DeploymentWizardContext( public OperatingSystemCatalogViewModel OperatingSystemCatalog { get; } public DriverPackSelectionViewModel DriverPackSelection { get; } public string? DefaultTimeZoneId { get; private set; } + public DeployOsRecoverySettings OsRecovery { get; private set; } = new(); public CoreDeployNetworkSettings Network { get; private set; } = new(); public DeployOobeSettings Oobe { get; private set; } = new(); public DeployAppxRemovalSettings AppxRemoval { get; private set; } = new(); @@ -110,6 +111,7 @@ private void ApplyDeployConfiguration( string.IsNullOrWhiteSpace(Preparation.TargetComputerName) ? seedComputerName : Preparation.TargetComputerName); + OsRecovery = document.OsRecovery ?? new DeployOsRecoverySettings(); Network = document.Network ?? new CoreDeployNetworkSettings(); Oobe = document.Customization.Oobe ?? new DeployOobeSettings(); AppxRemoval = document.Customization.AppxRemoval ?? new DeployAppxRemovalSettings(); diff --git a/src/Foundry.Deploy/Strings/ar-SA/Resources.resx b/src/Foundry.Deploy/Strings/ar-SA/Resources.resx index 09e20442..19cba0d5 100644 --- a/src/Foundry.Deploy/Strings/ar-SA/Resources.resx +++ b/src/Foundry.Deploy/Strings/ar-SA/Resources.resx @@ -509,4 +509,25 @@ تم تجهيز مساعد تسجيل Autopilot التفاعلي (محاكاة). + تأكيد استرداد نظام التشغيل + سيحتفظ استرداد نظام التشغيل بأقسام EFI وMSR والاسترداد، ثم يستبدل قسم Windows على القرص المحدد. + +القرص: {0} +الموديل: {1} +الحافلة: {2} +الحجم: {3} +نظام التشغيل: {4} + +هل تريد الاستمرار في استرداد نظام التشغيل؟ + توفير استرداد نظام التشغيل + جارٍ توفير استرداد نظام التشغيل... + جارٍ إدخال حمولة استرداد نظام التشغيل... + جارٍ حفظ تغييرات WinRE... + جارٍ تسجيل قائمة تمهيد استرداد نظام التشغيل... + تم تعطيل استرداد نظام التشغيل. + يتم تخطي توفير استرداد نظام التشغيل في وضع الاسترداد. + تم توفير استرداد نظام التشغيل. + تم توفير استرداد نظام التشغيل (محاكاة). + استرداد Foundry OSD + إعادة نشر Windows للطوارئ diff --git a/src/Foundry.Deploy/Strings/bg-BG/Resources.resx b/src/Foundry.Deploy/Strings/bg-BG/Resources.resx index ed155386..39af4b4b 100644 --- a/src/Foundry.Deploy/Strings/bg-BG/Resources.resx +++ b/src/Foundry.Deploy/Strings/bg-BG/Resources.resx @@ -509,4 +509,25 @@ Интерактивният помощник за регистрация на Autopilot е подготвен (симулация). + Потвърдете възстановяването на ОС + Възстановяването на ОС ще запази EFI, MSR и дяловете за възстановяване, след което ще замени дяла на Windows на избрания диск. + +Диск: {0} +Модел: {1} +Автобус: {2} +Размер: {3} +Операционна система: {4} + +Продължаване с възстановяване на ОС? + Осигуряване на възстановяване на ОС + Осигуряване на възстановяване на ОС... + Инжектиране на полезния товар за възстановяване на ОС... + Запазване на промените в WinRE... + Регистриране на менюто за стартиране на възстановяване на ОС... + Възстановяването на ОС е деактивирано. + Осигуряването на възстановяване на ОС се пропуска в режим на възстановяване. + Осигурено възстановяване на ОС. + Осигурено възстановяване на ОС (симулация). + Възстановяване Foundry OSD + Аварийно внедряване Windows diff --git a/src/Foundry.Deploy/Strings/cs-CZ/Resources.resx b/src/Foundry.Deploy/Strings/cs-CZ/Resources.resx index c3d75e16..0a1a5892 100644 --- a/src/Foundry.Deploy/Strings/cs-CZ/Resources.resx +++ b/src/Foundry.Deploy/Strings/cs-CZ/Resources.resx @@ -509,4 +509,25 @@ Pokračovat v nasazování? Interaktivní asistent registrace Autopilotu byl připraven (simulace). + Potvrďte obnovení OS + Obnova OS zachová oddíly EFI, MSR a Recovery a poté nahradí oddíl Windows na vybraném disku. + +Disk: {0} +Model: {1} +Autobus: {2} +Velikost: {3} +Operační systém: {4} + +Pokračovat v obnově OS? + Připravit obnovení OS + Provádění obnovy OS... + Vkládání dat pro obnovu OS... + Ukládání změn WinRE... + Registrace spouštěcí nabídky OS Recovery... + Obnovení OS je zakázáno. + Poskytování obnovy OS je v režimu obnovy přeskočeno. + Obnovení OS zajištěno. + Bylo zajištěno obnovení OS (simulace). + Obnova Foundry OSD + Nouzově znovu nasadit Windows diff --git a/src/Foundry.Deploy/Strings/da-DK/Resources.resx b/src/Foundry.Deploy/Strings/da-DK/Resources.resx index 327f25ad..44961573 100644 --- a/src/Foundry.Deploy/Strings/da-DK/Resources.resx +++ b/src/Foundry.Deploy/Strings/da-DK/Resources.resx @@ -51,7 +51,7 @@ Interaktiv upload af hardwarehash Tilgængelig på dette medie: {0} Zero-touch upload af hardwarehash - Hardware hash upload status + Status for upload af hardware-hash Klar til at uploade hardware-hash Ikke klar til at uploade hardware-hash Uploadcertifikat er udløbet @@ -509,4 +509,25 @@ Vil du fortsætte med implementeringen? Den interaktive Autopilot-registreringsassistent er klargjort (simulation). + Bekræft OS-gendannelse + OS Recovery bevarer EFI-, MSR- og Recovery-partitionerne og erstatter derefter Windows-partitionen på den valgte disk. + +Disk: {0} +Model: {1} +Bus: {2} +Størrelse: {3} +Operativsystem: {4} + +Vil du fortsætte med OS Recovery? + Provision OS Gendannelse + Klargøring af OS-gendannelse... + Injicerer OS-gendannelsesnyttelast... + Gemmer WinRE-ændringer... + Registrerer OS Recovery boot-menu... + OS-gendannelse er deaktiveret. + Klargøring af OS-gendannelse springes over i gendannelsestilstand. + OS-gendannelse klargjort. + OS-gendannelse klargjort (simulering). + Foundry OSD-gendannelse + Nødgenudrulning af Windows diff --git a/src/Foundry.Deploy/Strings/de-DE/Resources.resx b/src/Foundry.Deploy/Strings/de-DE/Resources.resx index c5b6ccfc..32575921 100644 --- a/src/Foundry.Deploy/Strings/de-DE/Resources.resx +++ b/src/Foundry.Deploy/Strings/de-DE/Resources.resx @@ -509,4 +509,25 @@ Mit der Bereitstellung fortfahren? Der interaktive Autopilot-Registrierungsassistent wurde bereitgestellt (Simulation). + Bestätigen Sie die Betriebssystemwiederherstellung + OS Recovery behält die EFI-, MSR- und Wiederherstellungspartitionen bei und ersetzt dann die Windows-Partition auf der ausgewählten Festplatte. + +Datenträger: {0} +Modell: {1} +Bus: {2} +Größe: {3} +Betriebssystem: {4} + +Mit der Betriebssystemwiederherstellung fortfahren? + Bereitstellen der Betriebssystemwiederherstellung + Betriebssystemwiederherstellung wird bereitgestellt... + Nutzlast für die Betriebssystemwiederherstellung wird eingefügt... + WinRE-Änderungen werden gespeichert... + Das Bootmenü für die Betriebssystemwiederherstellung wird registriert... + Die Betriebssystemwiederherstellung ist deaktiviert. + Die Bereitstellung der Betriebssystemwiederherstellung wird im Wiederherstellungsmodus übersprungen. + Betriebssystemwiederherstellung bereitgestellt. + Betriebssystemwiederherstellung bereitgestellt (Simulation). + Foundry OSD-Wiederherstellung + Windows-Notfallbereitstellung diff --git a/src/Foundry.Deploy/Strings/el-GR/Resources.resx b/src/Foundry.Deploy/Strings/el-GR/Resources.resx index 84071fb0..9f5d4268 100644 --- a/src/Foundry.Deploy/Strings/el-GR/Resources.resx +++ b/src/Foundry.Deploy/Strings/el-GR/Resources.resx @@ -31,7 +31,7 @@ Ανοίξτε το αρχείο καταγραφής 1. Στόχος 2. Λειτουργικό σύστημα - 3. Driver pack + 3. Πακέτο προγραμμάτων οδήγησης 4. Περίληψη Όνομα υπολογιστή 1-15 χαρακτήρες. A-Z, a-z, 0-9 και μόνο παύλα. @@ -509,4 +509,25 @@ Ο διαδραστικός βοηθός εγγραφής Autopilot προετοιμάστηκε (προσομοίωση). + Επιβεβαιώστε την ανάκτηση λειτουργικού συστήματος + Το OS Recovery θα διατηρήσει τα διαμερίσματα EFI, MSR και Recovery και, στη συνέχεια, θα αντικαταστήσει το διαμέρισμα των Windows στον επιλεγμένο δίσκο. + +Δίσκος: {0} +Μοντέλο: {1} +Λεωφορείο: {2} +Μέγεθος: {3} +Λειτουργικό σύστημα: {4} + +Συνέχεια με το OS Recovery; + Παροχή ανάκτησης λειτουργικού συστήματος + Παροχή ανάκτησης λειτουργικού συστήματος... + Έγχυση ωφέλιμου φορτίου ανάκτησης λειτουργικού συστήματος... + Αποθήκευση αλλαγών WinRE... + Εγγραφή του μενού εκκίνησης του OS Recovery... + Η ανάκτηση λειτουργικού συστήματος είναι απενεργοποιημένη. + Η παροχή ανάκτησης λειτουργικού συστήματος παραλείπεται στη λειτουργία ανάκτησης. + Παρέχεται ανάκτηση λειτουργικού συστήματος. + Παρέχεται ανάκτηση λειτουργικού συστήματος (προσομοίωση). + Ανάκτηση Foundry OSD + Επείγουσα ανάπτυξη Windows diff --git a/src/Foundry.Deploy/Strings/en-GB/Resources.resx b/src/Foundry.Deploy/Strings/en-GB/Resources.resx index 8c2b199f..08907663 100644 --- a/src/Foundry.Deploy/Strings/en-GB/Resources.resx +++ b/src/Foundry.Deploy/Strings/en-GB/Resources.resx @@ -509,4 +509,25 @@ Continue with deployment? Interactive Autopilot registration assistant staged (simulation). + Confirm OS Recovery + OS Recovery will preserve EFI, MSR, and Recovery partitions, then replace the Windows partition on the selected disk. + +Disk: {0} +Model: {1} +Bus: {2} +Size: {3} +Operating system: {4} + +Continue with OS Recovery? + Provision OS Recovery + Provisioning OS Recovery... + Injecting OS Recovery payload... + Committing WinRE changes... + Registering OS Recovery boot menu... + OS Recovery is disabled. + OS Recovery provisioning is skipped in recovery mode. + OS Recovery provisioned. + OS Recovery provisioned (simulation). + Foundry OSD Recovery + Emergency Windows redeploy diff --git a/src/Foundry.Deploy/Strings/en-US/Resources.resx b/src/Foundry.Deploy/Strings/en-US/Resources.resx index 37dd655f..e3ab4056 100644 --- a/src/Foundry.Deploy/Strings/en-US/Resources.resx +++ b/src/Foundry.Deploy/Strings/en-US/Resources.resx @@ -509,4 +509,25 @@ Continue with deployment? Interactive Autopilot registration assistant staged (simulation). + Confirm OS Recovery + OS Recovery will preserve EFI, MSR, and Recovery partitions, then replace the Windows partition on the selected disk. + +Disk: {0} +Model: {1} +Bus: {2} +Size: {3} +Operating system: {4} + +Continue with OS Recovery? + Provision OS Recovery + Provisioning OS Recovery... + Injecting OS Recovery payload... + Committing WinRE changes... + Registering OS Recovery boot menu... + OS Recovery is disabled. + OS Recovery provisioning is skipped in recovery mode. + OS Recovery provisioned. + OS Recovery provisioned (simulation). + Foundry OSD Recovery + Emergency Windows redeploy diff --git a/src/Foundry.Deploy/Strings/es-ES/Resources.resx b/src/Foundry.Deploy/Strings/es-ES/Resources.resx index e9722e1a..725c4896 100644 --- a/src/Foundry.Deploy/Strings/es-ES/Resources.resx +++ b/src/Foundry.Deploy/Strings/es-ES/Resources.resx @@ -509,4 +509,25 @@ Sistema operativo: {4} Asistente de registro interactivo de Autopilot preparado (simulación). + Confirmar la recuperación del sistema operativo + OS Recovery preservará las particiones EFI, MSR y Recovery, luego reemplazará la partición de Windows en el disco seleccionado. + +Disco: {0} +Modelo: {1} +Autobús: {2} +Tamaño: {3} +Sistema operativo: {4} + +¿Continuar con la recuperación del sistema operativo? + Aprovisionar recuperación del sistema operativo + Aprovisionando recuperación del sistema operativo... + Inyectando carga útil de recuperación del sistema operativo... + Guardando cambios de WinRE... + Registrando el menú de inicio de OS Recovery... + La recuperación del sistema operativo está deshabilitada. + El aprovisionamiento de OS Recovery se omite en el modo de recuperación. + Recuperación del sistema operativo aprovisionada. + Recuperación del sistema operativo aprovisionada (simulación). + Recuperación Foundry OSD + Reinstalación Windows urgente diff --git a/src/Foundry.Deploy/Strings/es-MX/Resources.resx b/src/Foundry.Deploy/Strings/es-MX/Resources.resx index 471f2d37..460fe012 100644 --- a/src/Foundry.Deploy/Strings/es-MX/Resources.resx +++ b/src/Foundry.Deploy/Strings/es-MX/Resources.resx @@ -509,4 +509,25 @@ Sistema operativo: {4} Asistente de registro interactivo de Autopilot preparado (simulación). + Confirmar la recuperación del sistema operativo + OS Recovery preservará las particiones EFI, MSR y Recovery, luego reemplazará la partición de Windows en el disco seleccionado. + +Disco: {0} +Modelo: {1} +Autobús: {2} +Tamaño: {3} +Sistema operativo: {4} + +¿Continuar con la recuperación del sistema operativo? + Aprovisionar recuperación del sistema operativo + Aprovisionando recuperación del sistema operativo... + Inyectando carga útil de recuperación del sistema operativo... + Guardando cambios de WinRE... + Registrando el menú de inicio de OS Recovery... + La recuperación del sistema operativo está deshabilitada. + El aprovisionamiento de OS Recovery se omite en el modo de recuperación. + Recuperación del sistema operativo aprovisionada. + Recuperación del sistema operativo aprovisionada (simulación). + Recuperación Foundry OSD + Reinstalación Windows urgente diff --git a/src/Foundry.Deploy/Strings/et-EE/Resources.resx b/src/Foundry.Deploy/Strings/et-EE/Resources.resx index 04dfbecf..e51d1875 100644 --- a/src/Foundry.Deploy/Strings/et-EE/Resources.resx +++ b/src/Foundry.Deploy/Strings/et-EE/Resources.resx @@ -509,4 +509,25 @@ Kas jätkata juurutamisega? Interaktiivne Autopiloti registreerimisabiline on ette valmistatud (simulatsioon). + Kinnitage OS-i taastamine + OS-i taastamine säilitab EFI-, MSR- ja taastepartitsioonid ning asendab seejärel Windowsi partitsiooni valitud kettal. + +Ketas: {0} +Mudel: {1} +Buss: {2} +Suurus: {3} +Operatsioonisüsteem: {4} + +Kas jätkata OS-i taastamisega? + OS-i taastamise pakkumine + OS-i taastamise ettevalmistamine... + OS-i taastamise kasuliku koormuse sisestamine... + WinRE muudatuste salvestamine... + OS-i taastamise alglaadimismenüü registreerimine... + OS-i taastamine on keelatud. + OS-i taastamise varustamine jäetakse taasterežiimis vahele. + OS-i taastamine on ette nähtud. + OS-i taastamine on ette nähtud (simulatsioon). + Foundry OSD taaste + Windowsi avariijuurutus diff --git a/src/Foundry.Deploy/Strings/fi-FI/Resources.resx b/src/Foundry.Deploy/Strings/fi-FI/Resources.resx index 38510cff..169a38f5 100644 --- a/src/Foundry.Deploy/Strings/fi-FI/Resources.resx +++ b/src/Foundry.Deploy/Strings/fi-FI/Resources.resx @@ -509,4 +509,25 @@ Jatketaanko käyttöönottoa? Vuorovaikutteinen Autopilot-rekisteröintiavustaja on valmisteltu (simulointi). + Vahvista käyttöjärjestelmän palautus + OS Recovery säilyttää EFI-, MSR- ja Recovery-osiot ja korvaa sitten Windows-osion valitulla levyllä. + +Levy: {0} +Malli: {1} +Bussi: {2} +Koko: {3} +Käyttöjärjestelmä: {4} + +Jatketaanko käyttöjärjestelmän palautusta? + Tarjoa käyttöjärjestelmän palautus + Otetaan käyttöön käyttöjärjestelmän palautus... + Lisätään käyttöjärjestelmän palautusta... + Tallennetaan WinRE-muutoksia... + Rekisteröidään OS Recovery -käynnistysvalikkoa... + Käyttöjärjestelmän palautus on poistettu käytöstä. + Käyttöjärjestelmän palautuksen hallinta ohitetaan palautustilassa. + Käyttöjärjestelmän palautus varattu. + Käyttöjärjestelmän palautus varusteltu (simulaatio). + Foundry OSD -palautus + Windowsin hätäasennus diff --git a/src/Foundry.Deploy/Strings/fr-CA/Resources.resx b/src/Foundry.Deploy/Strings/fr-CA/Resources.resx index e8a4ed38..c4fadc45 100644 --- a/src/Foundry.Deploy/Strings/fr-CA/Resources.resx +++ b/src/Foundry.Deploy/Strings/fr-CA/Resources.resx @@ -76,7 +76,7 @@ Langue Canal de licence Édition (cible) - Architecture: {0} + Architecture : {0} Chargement des catalogues... Catalogues chargés: {0} entrées du système d'exploitation, {1} packs de pilotes. Échec du chargement du catalogue: {0} @@ -509,4 +509,25 @@ Continuer le déploiement? Assistant d'enregistrement Autopilot interactif préparé (simulation). + Confirmer la récupération du SE + La récupération du SE conservera les partitions EFI, MSR et Recovery, puis remplacera la partition Windows sur le disque sélectionné. + +Disque : {0} +Modèle : {1} +Bus : {2} +Taille : {3} +Système d’exploitation : {4} + +Continuer avec la récupération du SE ? + Provisionner la récupération du SE + Provisionnement de la récupération du SE... + Injection du payload de récupération du SE... + Validation des modifications WinRE... + Enregistrement du menu de démarrage de récupération du SE... + La récupération du SE est désactivée. + Le provisionnement de la récupération du SE est ignoré en mode récupération. + Récupération du SE provisionnée. + Récupération du SE provisionnée (simulation). + Récupération Foundry OSD + Redéployer Windows d’urgence diff --git a/src/Foundry.Deploy/Strings/fr-FR/Resources.resx b/src/Foundry.Deploy/Strings/fr-FR/Resources.resx index 4a93b4a9..fa825cd5 100644 --- a/src/Foundry.Deploy/Strings/fr-FR/Resources.resx +++ b/src/Foundry.Deploy/Strings/fr-FR/Resources.resx @@ -509,4 +509,25 @@ Continuer le déploiement ? Assistant d'enregistrement Autopilot interactif préparé (simulation). + Confirmer la récupération de l’OS + La récupération de l’OS conservera les partitions EFI, MSR et Recovery, puis remplacera la partition Windows sur le disque sélectionné. + +Disque : {0} +Modèle : {1} +Bus : {2} +Taille : {3} +Système d’exploitation : {4} + +Continuer avec la récupération de l’OS ? + Provisionner la récupération de l’OS + Provisionnement de la récupération de l’OS... + Injection du payload de récupération de l’OS... + Validation des modifications WinRE... + Enregistrement du menu de démarrage de récupération de l’OS... + La récupération de l’OS est désactivée. + Le provisionnement de la récupération de l’OS est ignoré en mode récupération. + Récupération de l’OS provisionnée. + Récupération de l’OS provisionnée (simulation). + Récupération Foundry OSD + Redéployer Windows d’urgence diff --git a/src/Foundry.Deploy/Strings/he-IL/Resources.resx b/src/Foundry.Deploy/Strings/he-IL/Resources.resx index cb65f64a..d8682971 100644 --- a/src/Foundry.Deploy/Strings/he-IL/Resources.resx +++ b/src/Foundry.Deploy/Strings/he-IL/Resources.resx @@ -509,4 +509,25 @@ ErrorCode=0x80070005 מסייע הרישום האינטראקטיבי של Autopilot הוכן (סימולציה). + אשר את שחזור מערכת ההפעלה + שחזור מערכת ההפעלה ישמור מחיצות EFI, MSR ושחזור, ולאחר מכן יחליף את מחיצת Windows בדיסק הנבחר. + +דיסק: {0} +דגם: {1} +אוטובוס: {2} +גודל: {3} +מערכת הפעלה: {4} + +להמשיך עם שחזור מערכת ההפעלה? + שחזור מערכת הפעלה אספקה + הקצאת שחזור מערכת ההפעלה... + מזריקים מטען שחזור מערכת ההפעלה... + שומר שינויים ב-WinRE... + רושם את תפריט האתחול של שחזור מערכת ההפעלה... + שחזור מערכת ההפעלה מושבת. + דילוג על הקצאת שחזור מערכת ההפעלה במצב שחזור. + שחזור מערכת הפעלה הוקצה. + התאוששות מערכת הפעלה הוגדרה (סימולציה). + שחזור Foundry OSD + פריסת Windows בחירום diff --git a/src/Foundry.Deploy/Strings/hr-HR/Resources.resx b/src/Foundry.Deploy/Strings/hr-HR/Resources.resx index de02ecf2..bd437ffd 100644 --- a/src/Foundry.Deploy/Strings/hr-HR/Resources.resx +++ b/src/Foundry.Deploy/Strings/hr-HR/Resources.resx @@ -509,4 +509,25 @@ Nastaviti s implementacijom? Interaktivni pomoćnik za registraciju Autopilota je pripremljen (simulacija). + Potvrdite oporavak OS-a + OS Recovery će sačuvati EFI, MSR i particije za oporavak, a zatim zamijeniti Windows particiju na odabranom disku. + +Disk: {0} +Model: {1} +Autobus: {2} +Veličina: {3} +Operativni sustav: {4} + +Nastaviti s oporavkom OS-a? + Pružanje oporavka OS-a + Omogućavanje oporavka OS-a... + Ubacivanje nosivosti oporavka OS-a... + Spremanje WinRE promjena... + Registriranje izbornika za pokretanje OS Recovery... + Oporavak OS-a je onemogućen. + Omogućavanje oporavka OS-a preskočeno je u načinu oporavka. + Omogućen oporavak OS-a. + Osiguran oporavak OS-a (simulacija). + Foundry OSD oporavak + Hitno uvođenje Windowsa diff --git a/src/Foundry.Deploy/Strings/hu-HU/Resources.resx b/src/Foundry.Deploy/Strings/hu-HU/Resources.resx index 22a6da16..ff3f345e 100644 --- a/src/Foundry.Deploy/Strings/hu-HU/Resources.resx +++ b/src/Foundry.Deploy/Strings/hu-HU/Resources.resx @@ -509,4 +509,25 @@ Folytatja a telepítést? Az interaktív Autopilot regisztrációs segéd előkészítve (szimuláció). + Erősítse meg az operációs rendszer helyreállítását + Az OS Recovery megőrzi az EFI, MSR és Recovery partíciókat, majd lecseréli a Windows partíciót a kiválasztott lemezen. + +Lemez: {0} +Modell: {1} +Busz: {2} +Méret: {3} +Operációs rendszer: {4} + +Folytatja az OS helyreállítást? + Az operációs rendszer helyreállításának biztosítása + Az operációs rendszer helyreállításának kiépítése... + OS Recovery hasznos terhelés beadása... + WinRE-módosítások mentése... + Az OS Recovery rendszerindító menü regisztrálása... + Az operációs rendszer helyreállítása le van tiltva. + Az OS-helyreállítás kiépítése helyreállítási módban kimarad. + Az operációs rendszer helyreállítása biztosított. + Az operációs rendszer helyreállítása biztosított (szimuláció). + Foundry OSD-helyreállítás + Windows vészhelyzeti telepítés diff --git a/src/Foundry.Deploy/Strings/it-IT/Resources.resx b/src/Foundry.Deploy/Strings/it-IT/Resources.resx index 9d53f58d..262bded0 100644 --- a/src/Foundry.Deploy/Strings/it-IT/Resources.resx +++ b/src/Foundry.Deploy/Strings/it-IT/Resources.resx @@ -509,4 +509,25 @@ Continuare con la distribuzione? Assistente di registrazione interattiva di Autopilot preparato (simulazione). + Conferma il ripristino del sistema operativo + OS Recovery conserverà le partizioni EFI, MSR e Recovery, quindi sostituirà la partizione Windows sul disco selezionato. + +Disco: {0} +Modello: {1} +Autobus: {2} +Taglia: {3} +Sistema operativo: {4} + +Continuare con il ripristino del sistema operativo? + Fornire il ripristino del sistema operativo + Provisioning del ripristino del sistema operativo... + Inserimento del payload di ripristino del sistema operativo in corso... + Salvataggio delle modifiche WinRE... + Registrazione del menu di avvio di ripristino del sistema operativo in corso... + Il ripristino del sistema operativo è disabilitato. + Il provisioning del ripristino del sistema operativo viene ignorato in modalità di ripristino. + Effettuato il provisioning del ripristino del sistema operativo. + Provisioning del ripristino del sistema operativo (simulazione). + Ripristino Foundry OSD + Ripristino Windows urgente diff --git a/src/Foundry.Deploy/Strings/ja-JP/Resources.resx b/src/Foundry.Deploy/Strings/ja-JP/Resources.resx index 3f334a4f..0dc56b3c 100644 --- a/src/Foundry.Deploy/Strings/ja-JP/Resources.resx +++ b/src/Foundry.Deploy/Strings/ja-JP/Resources.resx @@ -509,4 +509,25 @@ 対話型 Autopilot 登録アシスタントをステージングしました (シミュレーション)。 + OSリカバリの確認 + OS リカバリでは、EFI、MSR、およびリカバリ パーティションが保存され、選択したディスク上の Windows パーティションが置き換えられます。 + +ディスク: {0} +モデル: {1} +バス: {2} +サイズ: {3} +オペレーティング システム: {4} + +OS リカバリを続行しますか? + OS リカバリのプロビジョニング + OS リカバリをプロビジョニングしています... + OS リカバリ ペイロードを挿入しています... + WinRE の変更を保存しています... + OS リカバリ ブート メニューを登録しています... + OS リカバリは無効になっています。 + OS リカバリ プロビジョニングはリカバリ モードではスキップされます。 + OS リカバリがプロビジョニングされました。 + OS リカバリがプロビジョニングされました (シミュレーション)。 + Foundry OSD 回復 + 緊急 Windows 再展開 diff --git a/src/Foundry.Deploy/Strings/ko-KR/Resources.resx b/src/Foundry.Deploy/Strings/ko-KR/Resources.resx index b1d24ab4..fd108c69 100644 --- a/src/Foundry.Deploy/Strings/ko-KR/Resources.resx +++ b/src/Foundry.Deploy/Strings/ko-KR/Resources.resx @@ -509,4 +509,25 @@ 대화형 Autopilot 등록 도우미가 준비되었습니다(시뮬레이션). + OS 복구 확인 + OS 복구는 EFI, MSR 및 복구 파티션을 보존한 다음 선택한 디스크의 Windows 파티션을 교체합니다. + +디스크: {0} +모델: {1} +버스: {2} +크기: {3} +운영 체제: {4} + +OS 복구를 계속하시겠습니까? + OS 복구 프로비저닝 + OS 복구 프로비저닝 중... + OS 복구 페이로드를 삽입하는 중... + WinRE 변경 내용을 저장하는 중... + OS 복구 부팅 메뉴 등록 중... + OS 복구가 비활성화되었습니다. + 복구 모드에서는 OS 복구 프로비저닝을 건너뜁니다. + OS 복구가 프로비저닝되었습니다. + OS 복구 프로비저닝됨(시뮬레이션) + Foundry OSD 복구 + 긴급 Windows 재배포 diff --git a/src/Foundry.Deploy/Strings/lt-LT/Resources.resx b/src/Foundry.Deploy/Strings/lt-LT/Resources.resx index b7cfbbc7..bfd1be4a 100644 --- a/src/Foundry.Deploy/Strings/lt-LT/Resources.resx +++ b/src/Foundry.Deploy/Strings/lt-LT/Resources.resx @@ -509,4 +509,25 @@ Tęsti diegimą? Interaktyvus Autopilot registracijos pagalbininkas paruoštas (modeliavimas). + Patvirtinkite OS atkūrimą + OS atkūrimas išsaugos EFI, MSR ir Recovery skaidinius, tada pakeis Windows skaidinį pasirinktame diske. + +Diskas: {0} +Modelis: {1} +Autobusas: {2} +Dydis: {3} +Operacinė sistema: {4} + +Tęsti OS atkūrimą? + Pateikite OS atkūrimą + Teikiamas OS atkūrimas... + Įvedama OS atkūrimo naudingoji apkrova... + Įrašomi WinRE pakeitimai... + Registruojamas OS atkūrimo įkrovos meniu... + OS atkūrimas išjungtas. + Atkūrimo režimu OS atkūrimo aprūpinimas praleidžiamas. + Suteiktas OS atkūrimas. + OS atkūrimas numatytas (modeliavimas). + Foundry OSD atkūrimas + Avarinis Windows diegimas diff --git a/src/Foundry.Deploy/Strings/lv-LV/Resources.resx b/src/Foundry.Deploy/Strings/lv-LV/Resources.resx index 91da3fe9..9ebfd43e 100644 --- a/src/Foundry.Deploy/Strings/lv-LV/Resources.resx +++ b/src/Foundry.Deploy/Strings/lv-LV/Resources.resx @@ -444,7 +444,7 @@ Vai turpināt izvietošanu? latviešu (Latvija) - Norwegian Bokmal (Norway) + norvēģu bukmols (Norvēģija) holandiešu (Nīderlande) @@ -509,4 +509,25 @@ Vai turpināt izvietošanu? Interaktīvais Autopilot reģistrācijas palīgs ir sagatavots (simulācija). + Apstipriniet OS atkopšanu + OS atkopšana saglabās EFI, MSR un Recovery nodalījumus, pēc tam nomainīs Windows nodalījumu atlasītajā diskā. + +Disks: {0} +Modelis: {1} +Autobuss: {2} +Izmērs: {3} +Operētājsistēma: {4} + +Vai turpināt ar OS atkopšanu? + Nodrošiniet OS atkopšanu + Notiek OS atkopšanas nodrošināšana... + Notiek OS atkopšanas derīgās slodzes ievadīšana... + Tiek saglabātas WinRE izmaiņas... + Notiek OS atkopšanas sāknēšanas izvēlnes reģistrācija... + OS atkopšana ir atspējota. + Atkopšanas režīmā OS atkopšanas nodrošināšana tiek izlaista. + Nodrošināta OS atkopšana. + Nodrošināta OS atkopšana (simulācija). + Foundry OSD atkopšana + Windows ārkārtas izvietošana diff --git a/src/Foundry.Deploy/Strings/nb-NO/Resources.resx b/src/Foundry.Deploy/Strings/nb-NO/Resources.resx index f5094bd1..0aa4e553 100644 --- a/src/Foundry.Deploy/Strings/nb-NO/Resources.resx +++ b/src/Foundry.Deploy/Strings/nb-NO/Resources.resx @@ -509,4 +509,25 @@ Vil du fortsette med distribusjonen? Den interaktive Autopilot-registreringsassistenten er klargjort (simulering). + Bekreft OS-gjenoppretting + OS Recovery vil bevare EFI-, MSR- og Recovery-partisjonene, og erstatter deretter Windows-partisjonen på den valgte disken. + +Disk: {0} +Modell: {1} +Buss: {2} +Størrelse: {3} +Operativsystem: {4} + +Vil du fortsette med OS-gjenoppretting? + Klargjør OS-gjenoppretting + Klargjør OS-gjenoppretting... + Injiserer nyttelast for OS-gjenoppretting... + Lagrer WinRE-endringer... + Registrerer OS Recovery oppstartsmeny... + OS-gjenoppretting er deaktivert. + Klargjøring av OS-gjenoppretting hoppes over i gjenopprettingsmodus. + OS-gjenoppretting klargjort. + OS-gjenoppretting klargjort (simulering). + Foundry OSD-gjenoppretting + Nødutrulling av Windows diff --git a/src/Foundry.Deploy/Strings/nl-NL/Resources.resx b/src/Foundry.Deploy/Strings/nl-NL/Resources.resx index 71cd0e66..7c1fa6ea 100644 --- a/src/Foundry.Deploy/Strings/nl-NL/Resources.resx +++ b/src/Foundry.Deploy/Strings/nl-NL/Resources.resx @@ -509,4 +509,25 @@ Doorgaan met de implementatie? Interactieve Autopilot-registratieassistent voorbereid (simulatie). + Bevestig OS-herstel + OS Recovery behoudt de EFI-, MSR- en herstelpartities en vervangt vervolgens de Windows-partitie op de geselecteerde schijf. + +Schijf: {0} +Model: {1} +Bus: {2} +Maat: {3} +Besturingssysteem: {4} + +Doorgaan met OS-herstel? + OS-herstel inrichten + Besturingssysteemherstel inrichten... + Bezig met injecteren van de OS Recovery-payload... + WinRE-wijzigingen worden opgeslagen... + Registreren van het opstartmenu van OS Recovery... + Besturingssysteemherstel is uitgeschakeld. + Het inrichten van OS Recovery wordt overgeslagen in de herstelmodus. + OS-herstel ingericht. + OS Recovery ingericht (simulatie). + Foundry OSD-herstel + Windows nooduitrol diff --git a/src/Foundry.Deploy/Strings/pl-PL/Resources.resx b/src/Foundry.Deploy/Strings/pl-PL/Resources.resx index ff1a14d7..c8687543 100644 --- a/src/Foundry.Deploy/Strings/pl-PL/Resources.resx +++ b/src/Foundry.Deploy/Strings/pl-PL/Resources.resx @@ -509,4 +509,25 @@ Kontynuować wdrażanie? Interaktywny asystent rejestracji Autopilot został przygotowany (symulacja). + Potwierdź przywrócenie systemu operacyjnego + Funkcja OS Recovery zachowa partycje EFI, MSR i Recovery, a następnie zastąpi partycję Windows na wybranym dysku. + +Dysk: {0} +Modelka: {1} +Autobus: {2} +Rozmiar: {3} +System operacyjny: {4} + +Kontynuować odzyskiwanie systemu operacyjnego? + Zapewnij odzyskiwanie systemu operacyjnego + Udostępnianie odzyskiwania systemu operacyjnego... + Wstrzykiwanie ładunku odzyskiwania systemu operacyjnego... + Zapisywanie zmian WinRE... + Rejestrowanie menu startowego odzyskiwania systemu operacyjnego... + Odzyskiwanie systemu operacyjnego jest wyłączone. + Udostępnianie systemu operacyjnego OS Recovery jest pomijane w trybie odzyskiwania. + Zapewnione odzyskiwanie systemu operacyjnego. + Zapewnione odzyskiwanie systemu operacyjnego (symulacja). + Odzyskiwanie Foundry OSD + Awaryjne wdrożenie Windows diff --git a/src/Foundry.Deploy/Strings/pt-BR/Resources.resx b/src/Foundry.Deploy/Strings/pt-BR/Resources.resx index 1f9ff2fb..d58bf4f3 100644 --- a/src/Foundry.Deploy/Strings/pt-BR/Resources.resx +++ b/src/Foundry.Deploy/Strings/pt-BR/Resources.resx @@ -509,4 +509,25 @@ Continuar com a implantação? Assistente de registro interativo do Autopilot preparado (simulação). + Confirme a recuperação do sistema operacional + O OS Recovery preservará as partições EFI, MSR e Recovery e, em seguida, substituirá a partição do Windows no disco selecionado. + +Disco: {0} +Modelo: {1} +Ônibus: {2} +Tamanho: {3} +Sistema operacional: {4} + +Continuar com a recuperação do sistema operacional? + Provisionar recuperação de sistema operacional + Provisionando recuperação do sistema operacional... + Injetando carga útil de recuperação do sistema operacional... + Salvando alterações do WinRE... + Registrando o menu de inicialização do OS Recovery... + A recuperação do sistema operacional está desativada. + O provisionamento do OS Recovery é ignorado no modo de recuperação. + Recuperação do sistema operacional provisionada. + Recuperação de SO provisionada (simulação). + Recuperação Foundry OSD + Reimplantar Windows urgente diff --git a/src/Foundry.Deploy/Strings/pt-PT/Resources.resx b/src/Foundry.Deploy/Strings/pt-PT/Resources.resx index 437ef9a4..404987b5 100644 --- a/src/Foundry.Deploy/Strings/pt-PT/Resources.resx +++ b/src/Foundry.Deploy/Strings/pt-PT/Resources.resx @@ -509,4 +509,25 @@ Continuar com a implantação? Assistente de registo interativo do Autopilot preparado (simulação). + Confirme a recuperação do sistema operacional + O OS Recovery preservará as partições EFI, MSR e Recovery e, em seguida, substituirá a partição do Windows no disco selecionado. + +Disco: {0} +Modelo: {1} +Ônibus: {2} +Tamanho: {3} +Sistema operacional: {4} + +Continuar com a recuperação do sistema operacional? + Provisionar recuperação de sistema operacional + Provisionando recuperação do sistema operacional... + Injetando carga útil de recuperação do sistema operacional... + A guardar alterações do WinRE... + Registrando o menu de inicialização do OS Recovery... + A recuperação do sistema operacional está desativada. + O provisionamento do OS Recovery é ignorado no modo de recuperação. + Recuperação do sistema operacional provisionada. + Recuperação de SO provisionada (simulação). + Recuperação Foundry OSD + Reinstalar Windows urgente diff --git a/src/Foundry.Deploy/Strings/ro-RO/Resources.resx b/src/Foundry.Deploy/Strings/ro-RO/Resources.resx index 8e259b4d..2761b48c 100644 --- a/src/Foundry.Deploy/Strings/ro-RO/Resources.resx +++ b/src/Foundry.Deploy/Strings/ro-RO/Resources.resx @@ -509,4 +509,25 @@ Continuați cu implementarea? Asistentul de înregistrare interactivă Autopilot a fost pregătit (simulare). + Confirmați recuperarea sistemului de operare + OS Recovery va păstra partițiile EFI, MSR și Recovery, apoi va înlocui partiția Windows de pe discul selectat. + +Disc: {0} +Model: {1} +Autobuz: {2} +Dimensiune: {3} +Sistem de operare: {4} + +Continuați cu OS Recovery? + Asigurați recuperarea sistemului de operare + Se asigură recuperarea sistemului de operare... + Se injectează sarcina utilă de recuperare a sistemului de operare... + Se salvează modificările WinRE... + Se înregistrează meniul de pornire OS Recovery... + Recuperarea sistemului de operare este dezactivată. + Aprovizionarea OS Recovery este omisă în modul de recuperare. + Recuperarea sistemului de operare a fost asigurată. + Recuperare OS asigurată (simulare). + Recuperare Foundry OSD + Redeploy Windows de urgență diff --git a/src/Foundry.Deploy/Strings/ru-RU/Resources.resx b/src/Foundry.Deploy/Strings/ru-RU/Resources.resx index a4b66ad5..94879209 100644 --- a/src/Foundry.Deploy/Strings/ru-RU/Resources.resx +++ b/src/Foundry.Deploy/Strings/ru-RU/Resources.resx @@ -509,4 +509,25 @@ Интерактивный помощник регистрации Autopilot подготовлен (симуляция). + Подтвердить восстановление ОС + Восстановление ОС сохранит разделы EFI, MSR и Recovery, а затем заменит раздел Windows на выбранном диске. + +Диск: {0} +Модель: {1} +Автобус: {2} +Размер: {3} +Операционная система: {4} + +Продолжить восстановление ОС? + Обеспечение восстановления ОС + Обеспечение восстановления ОС... + Внедрение полезных данных восстановления ОС... + Сохранение изменений WinRE... + Регистрация меню загрузки OS Recovery... + Восстановление ОС отключено. + В режиме восстановления подготовка ОС для восстановления пропускается. + Предусмотрено восстановление ОС. + Предусмотрено восстановление ОС (симуляция). + Восстановление Foundry OSD + Аварийная установка Windows diff --git a/src/Foundry.Deploy/Strings/sk-SK/Resources.resx b/src/Foundry.Deploy/Strings/sk-SK/Resources.resx index d9231902..6e766167 100644 --- a/src/Foundry.Deploy/Strings/sk-SK/Resources.resx +++ b/src/Foundry.Deploy/Strings/sk-SK/Resources.resx @@ -509,4 +509,25 @@ Pokračovať v nasadzovaní? Interaktívny asistent registrácie Autopilotu bol pripravený (simulácia). + Potvrďte obnovenie OS + Obnova OS zachová oblasti EFI, MSR a Recovery a potom nahradí oblasť Windows na vybranom disku. + +Disk: {0} +Model: {1} +Autobus: {2} +Veľkosť: {3} +Operačný systém: {4} + +Pokračovať v obnove OS? + Poskytovanie obnovy OS + Poskytovanie obnovy OS... + Vkladá sa užitočné zaťaženie na obnovenie OS... + Ukladajú sa zmeny WinRE... + Registrácia zavádzacej ponuky obnovy OS... + Obnovenie OS je vypnuté. + Poskytovanie obnovy OS sa v režime obnovenia preskočí. + Zabezpečené obnovenie OS. + Zabezpečené obnovenie OS (simulácia). + Obnova Foundry OSD + Núdzové nasadenie Windows diff --git a/src/Foundry.Deploy/Strings/sl-SI/Resources.resx b/src/Foundry.Deploy/Strings/sl-SI/Resources.resx index 064ff8d4..89c7ec00 100644 --- a/src/Foundry.Deploy/Strings/sl-SI/Resources.resx +++ b/src/Foundry.Deploy/Strings/sl-SI/Resources.resx @@ -509,4 +509,25 @@ Ali želite nadaljevati z uvajanjem? Interaktivni pomočnik za registracijo Autopilot je pripravljen (simulacija). + Potrdite obnovitev OS + OS Recovery bo ohranil particije EFI, MSR in Recovery, nato pa zamenjal particijo Windows na izbranem disku. + +Disk: {0} +Model: {1} +Avtobus: {2} +Velikost: {3} +Operacijski sistem: {4} + +Nadaljevati z obnovitvijo OS? + Zagotavljanje obnovitve OS + Omogočanje obnovitve OS ... + Vstavljanje tovora za obnovitev OS ... + Shranjevanje sprememb WinRE... + Registracija zagonskega menija za obnovitev OS ... + Obnovitev OS je onemogočena. + Omogočanje obnovitve OS je v obnovitvenem načinu preskočeno. + Zagotovljena obnovitev OS. + Zagotovljena obnovitev OS (simulacija). + Obnovitev Foundry OSD + Nujna uvedba Windows diff --git a/src/Foundry.Deploy/Strings/sr-Latn-RS/Resources.resx b/src/Foundry.Deploy/Strings/sr-Latn-RS/Resources.resx index 1825bcbc..2b24d27d 100644 --- a/src/Foundry.Deploy/Strings/sr-Latn-RS/Resources.resx +++ b/src/Foundry.Deploy/Strings/sr-Latn-RS/Resources.resx @@ -509,4 +509,25 @@ Operativni sistem: {4} Interaktivni pomoćnik za registraciju Autopilota je pripremljen (simulacija). + Potvrdi oporavak OS-a + Oporavak OS-a će zadržati EFI, MSR i Recovery particije, zatim zameniti Windows particiju na izabranom disku. + +Disk: {0} +Model: {1} +Bus: {2} +Veličina: {3} +Operativni sistem: {4} + +Nastaviti sa oporavkom OS-a? + Pripremi oporavak OS-a + Priprema oporavka OS-a... + Ubacivanje sadržaja oporavka OS-a... + Čuvanje WinRE promena... + Registrovanje menija za pokretanje oporavka OS-a... + Oporavak OS-a je onemogućen. + Priprema oporavka OS-a se preskače u režimu oporavka. + Oporavak OS-a je pripremljen. + Oporavak OS-a je pripremljen (simulacija). + Foundry OSD oporavak + Hitno uvođenje Windowsa diff --git a/src/Foundry.Deploy/Strings/sv-SE/Resources.resx b/src/Foundry.Deploy/Strings/sv-SE/Resources.resx index 4c63e91b..5d71174b 100644 --- a/src/Foundry.Deploy/Strings/sv-SE/Resources.resx +++ b/src/Foundry.Deploy/Strings/sv-SE/Resources.resx @@ -207,7 +207,7 @@ Fortsätt med implementeringen? Ladda ner firmwareuppdatering Applicera firmwareuppdatering Försegla återställningspartition - Provision Autopilot + Provisionera Autopilot Slutför distributionen och skriv loggar Samlar in implementeringsvariabler... Samlar in implementeringskontext... @@ -509,4 +509,25 @@ Fortsätt med implementeringen? Den interaktiva Autopilot-registreringsassistenten har förberetts (simulering). + Bekräfta OS-återställning + OS Recovery bevarar EFI-, MSR- och Recovery-partitionerna och ersätter sedan Windows-partitionen på den valda disken. + +Disk: {0} +Modell: {1} +Buss: {2} +Storlek: {3} +Operativsystem: {4} + +Fortsätt med OS Recovery? + Provisionera OS-återställning + Provisionerar OS-återställning... + Injicerar nyttolast för OS-återställning... + Sparar WinRE-ändringar... + Registrerar OS Recovery startmeny... + OS-återställning är inaktiverad. + Provisionering av OS-återställning hoppas över i återställningsläge. + OS-återställning tillhandahållen. + OS-återställning tillhandahållen (simulering). + Foundry OSD-återställning + Nöddistribuera Windows diff --git a/src/Foundry.Deploy/Strings/th-TH/Resources.resx b/src/Foundry.Deploy/Strings/th-TH/Resources.resx index b175ae96..4a77ce3e 100644 --- a/src/Foundry.Deploy/Strings/th-TH/Resources.resx +++ b/src/Foundry.Deploy/Strings/th-TH/Resources.resx @@ -509,4 +509,25 @@ จัดเตรียมผู้ช่วยลงทะเบียน Autopilot แบบโต้ตอบแล้ว (การจำลอง) + ยืนยันการกู้คืนระบบปฏิบัติการ + การกู้คืนระบบปฏิบัติการจะรักษาพาร์ติชัน EFI, MSR และการกู้คืนไว้ จากนั้นแทนที่พาร์ติชัน Windows บนดิสก์ที่เลือก + +ดิสก์: {0} +รุ่น: {1} +รถบัส: {2} +ขนาด: {3} +ระบบปฏิบัติการ: {4} + +ดำเนินการกู้คืน OS ต่อไปหรือไม่ + จัดเตรียมการกู้คืนระบบปฏิบัติการ + กำลังจัดเตรียมการกู้คืนระบบปฏิบัติการ... + กำลังเพิ่มเพย์โหลดการกู้คืน OS... + กำลังบันทึกการเปลี่ยนแปลง WinRE... + กำลังลงทะเบียนเมนูบูต OS Recovery... + การกู้คืนระบบปฏิบัติการถูกปิดใช้งาน + การจัดสรรการกู้คืนระบบปฏิบัติการถูกข้ามไปในโหมดการกู้คืน + จัดเตรียมการกู้คืนระบบปฏิบัติการแล้ว + จัดเตรียมการกู้คืน OS (การจำลอง) + กู้คืน Foundry OSD + ปรับใช้ Windows ฉุกเฉิน diff --git a/src/Foundry.Deploy/Strings/tr-TR/Resources.resx b/src/Foundry.Deploy/Strings/tr-TR/Resources.resx index 2eca99a9..4b59e15c 100644 --- a/src/Foundry.Deploy/Strings/tr-TR/Resources.resx +++ b/src/Foundry.Deploy/Strings/tr-TR/Resources.resx @@ -509,4 +509,25 @@ Dağıtıma devam edilsin mi? Etkileşimli Autopilot kayıt yardımcısı hazırlandı (simülasyon). + İşletim Sistemi Kurtarmayı Onayla + İşletim Sistemi Kurtarma, EFI, MSR ve Recovery bölümlerini koruyacak, ardından seçilen diskteki Windows bölümünü değiştirecektir. + +Disk: {0} +Modeli: {1} +Otobüs: {2} +Boyut: {3} +İşletim sistemi: {4} + +İşletim Sistemi Kurtarma işlemine devam edilsin mi? + İşletim Sistemi Kurtarmanın Sağlanması + İşletim Sistemi Kurtarma Sağlanıyor... + İşletim Sistemi Kurtarma yükü enjekte ediliyor... + WinRE değişiklikleri kaydediliyor... + İşletim Sistemi Kurtarma önyükleme menüsü kaydediliyor... + İşletim Sistemi Kurtarma devre dışı. + İşletim Sistemi Kurtarma provizyonu, kurtarma modunda atlanır. + İşletim Sistemi Kurtarma sağlandı. + İşletim Sistemi Kurtarmanın temel hazırlığı yapıldı (simülasyon). + Foundry OSD Kurtarma + Acil Windows yeniden dağıtımı diff --git a/src/Foundry.Deploy/Strings/uk-UA/Resources.resx b/src/Foundry.Deploy/Strings/uk-UA/Resources.resx index 4d78ec0b..5f980cbb 100644 --- a/src/Foundry.Deploy/Strings/uk-UA/Resources.resx +++ b/src/Foundry.Deploy/Strings/uk-UA/Resources.resx @@ -509,4 +509,25 @@ ErrorCode=0x80070005 Інтерактивний помічник реєстрації Autopilot підготовлено (симуляція). + Підтвердьте відновлення ОС + Відновлення ОС збереже розділи EFI, MSR і Recovery, а потім замінить розділ Windows на вибраному диску. + +Диск: {0} +Модель: {1} +Автобус: {2} +Розмір: {3} +Операційна система: {4} + +Продовжити відновлення ОС? + Надання відновлення ОС + Ініціалізація відновлення ОС... + Впровадження корисного навантаження відновлення ОС... + Збереження змін WinRE... + Реєстрація меню завантаження відновлення ОС... + Відновлення ОС вимкнено. + Ініціалізація відновлення ОС пропускається в режимі відновлення. + Забезпечено відновлення ОС. + Забезпечено відновлення ОС (симуляція). + Відновлення Foundry OSD + Аварійне розгортання Windows diff --git a/src/Foundry.Deploy/Strings/zh-CN/Resources.resx b/src/Foundry.Deploy/Strings/zh-CN/Resources.resx index bbff8202..99fe1e81 100644 --- a/src/Foundry.Deploy/Strings/zh-CN/Resources.resx +++ b/src/Foundry.Deploy/Strings/zh-CN/Resources.resx @@ -509,4 +509,25 @@ 交互式 Autopilot 注册助手已暂存(模拟)。 + 确认操作系统恢复 + 操作系统恢复将保留 EFI、MSR 和恢复分区,然后替换所选磁盘上的 Windows 分区。 + +磁盘:{0} +型号:{1} +巴士:{2} +尺寸:{3} +操作系统:{4} + +继续操作系统恢复吗? + 配置操作系统恢复 + 配置操作系统恢复... + 正在注入操作系统恢复负载... + 正在保存 WinRE 更改... + 正在注册操作系统恢复启动菜单... + 操作系统恢复已禁用。 + 在恢复模式下会跳过操作系统恢复配置。 + 已配置操作系统恢复。 + 已配置操作系统恢复(模拟)。 + Foundry OSD 恢复 + 紧急重新部署 Windows diff --git a/src/Foundry.Deploy/Strings/zh-TW/Resources.resx b/src/Foundry.Deploy/Strings/zh-TW/Resources.resx index f572b8fe..15735e96 100644 --- a/src/Foundry.Deploy/Strings/zh-TW/Resources.resx +++ b/src/Foundry.Deploy/Strings/zh-TW/Resources.resx @@ -509,4 +509,25 @@ 互動式 Autopilot 註冊助理已暫存 (模擬)。 + 確認作業系統恢復 + 作業系統復原將保留 EFI、MSR 和復原分割區,然後取代所選磁碟上的 Windows 分割區。 + +磁碟:{0} +型號:{1} +巴士:{2} +尺寸:{3} +作業系統:{4} + +繼續作業系統恢復嗎? + 配置作業系統恢復 + 配置作業系統恢復... + 正在註入作業系統恢復負載... + 正在儲存 WinRE 變更... + 正在註冊作業系統恢復啟動選單... + 作業系統恢復已停用。 + 在復原模式下會跳過作業系統恢復配置。 + 已配置作業系統恢復。 + 已配置作業系統恢復(模擬)。 + Foundry OSD 復原 + 緊急重新部署 Windows diff --git a/src/Foundry.Deploy/ViewModels/MainWindowViewModel.cs b/src/Foundry.Deploy/ViewModels/MainWindowViewModel.cs index 166ee38d..ae162a3f 100644 --- a/src/Foundry.Deploy/ViewModels/MainWindowViewModel.cs +++ b/src/Foundry.Deploy/ViewModels/MainWindowViewModel.cs @@ -275,6 +275,7 @@ private async Task StartDeploymentAsync() AutopilotProvisioningMode = Preparation.AutopilotProvisioningMode, SelectedAutopilotProfile = Preparation.SelectedAutopilotProfile, AutopilotHardwareHashUpload = Preparation.CreateAutopilotHardwareHashUploadForLaunch(), + OsRecovery = _wizardContext.OsRecovery, Network = _wizardContext.Network, Oobe = _wizardContext.Oobe, AppxRemoval = _wizardContext.AppxRemoval, diff --git a/src/Foundry.Telemetry.Tests/TelemetryBootMediaTargetResolverTests.cs b/src/Foundry.Telemetry.Tests/TelemetryBootMediaTargetResolverTests.cs index faf227dd..ba420253 100644 --- a/src/Foundry.Telemetry.Tests/TelemetryBootMediaTargetResolverTests.cs +++ b/src/Foundry.Telemetry.Tests/TelemetryBootMediaTargetResolverTests.cs @@ -9,8 +9,10 @@ public sealed class TelemetryBootMediaTargetResolverTests [InlineData(TelemetryRuntimeModes.Desktop, "Usb", TelemetryBootMediaTargets.None)] [InlineData(TelemetryRuntimeModes.WinPe, "Usb", TelemetryBootMediaTargets.Usb)] [InlineData(TelemetryRuntimeModes.WinPe, "Iso", TelemetryBootMediaTargets.Iso)] + [InlineData(TelemetryRuntimeModes.WinPe, "Recovery", TelemetryBootMediaTargets.Recovery)] [InlineData(TelemetryRuntimeModes.WinPe, "usb", TelemetryBootMediaTargets.Usb)] [InlineData(TelemetryRuntimeModes.WinPe, "iso", TelemetryBootMediaTargets.Iso)] + [InlineData(TelemetryRuntimeModes.WinPe, "recovery", TelemetryBootMediaTargets.Recovery)] [InlineData(TelemetryRuntimeModes.WinPe, null, TelemetryBootMediaTargets.Unknown)] [InlineData(TelemetryRuntimeModes.WinPe, "", TelemetryBootMediaTargets.Unknown)] [InlineData(TelemetryRuntimeModes.WinPe, "Foundry Cache", TelemetryBootMediaTargets.Unknown)] diff --git a/src/Foundry.Telemetry/TelemetryBootMediaTargetResolver.cs b/src/Foundry.Telemetry/TelemetryBootMediaTargetResolver.cs index 470f66a3..b2114546 100644 --- a/src/Foundry.Telemetry/TelemetryBootMediaTargetResolver.cs +++ b/src/Foundry.Telemetry/TelemetryBootMediaTargetResolver.cs @@ -22,6 +22,7 @@ public static string Resolve(string runtime, string? deploymentMode) { "iso" => TelemetryBootMediaTargets.Iso, "usb" => TelemetryBootMediaTargets.Usb, + "recovery" => TelemetryBootMediaTargets.Recovery, _ => TelemetryBootMediaTargets.Unknown }; } diff --git a/src/Foundry.Telemetry/TelemetryBootMediaTargets.cs b/src/Foundry.Telemetry/TelemetryBootMediaTargets.cs index c6248d2b..3e60fa05 100644 --- a/src/Foundry.Telemetry/TelemetryBootMediaTargets.cs +++ b/src/Foundry.Telemetry/TelemetryBootMediaTargets.cs @@ -7,6 +7,7 @@ public static class TelemetryBootMediaTargets { public const string Iso = "iso"; public const string Usb = "usb"; + public const string Recovery = "recovery"; public const string None = "none"; public const string Unknown = "unknown"; } diff --git a/src/Foundry/Assets/NavViewMenu/AppData.json b/src/Foundry/Assets/NavViewMenu/AppData.json index 3bcf6120..5b2ca328 100644 --- a/src/Foundry/Assets/NavViewMenu/AppData.json +++ b/src/Foundry/Assets/NavViewMenu/AppData.json @@ -102,6 +102,16 @@ "HideItem": true, "IsNew": false, "IsUpdated": false + }, + { + "UniqueId": "Foundry.Views.OsRecoveryPage", + "Title": "OS Recovery", + "LocalizeId": "Nav_OsRecoveryKey", + "UsexUid": true, + "IconGlyph": "E7BA", + "HideItem": true, + "IsNew": false, + "IsUpdated": false } ] }, diff --git a/src/Foundry/Assets/OsRecovery/winre-os-recovery-flow.png b/src/Foundry/Assets/OsRecovery/winre-os-recovery-flow.png new file mode 100644 index 00000000..0ae9ac9b Binary files /dev/null and b/src/Foundry/Assets/OsRecovery/winre-os-recovery-flow.png differ diff --git a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs index d513e9ac..88ae0f97 100644 --- a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs @@ -127,6 +127,7 @@ public static IServiceCollection AddFoundryApplicationServices(this IServiceColl services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Foundry/FoundryApplicationInfo.cs b/src/Foundry/FoundryApplicationInfo.cs index 188a5607..e70dd5a6 100644 --- a/src/Foundry/FoundryApplicationInfo.cs +++ b/src/Foundry/FoundryApplicationInfo.cs @@ -39,6 +39,11 @@ public static class FoundryApplicationInfo /// public const string NetworkDocumentationUrl = "https://foundry-osd.github.io/docs/configure/network"; + /// + /// Gets the OS Recovery documentation URL. + /// + public const string OsRecoveryDocumentationUrl = "https://foundry-osd.github.io/docs/deploy/os-recovery"; + /// /// Gets the media creation documentation URL. /// diff --git a/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs b/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs index b35cdb0a..ae94d12f 100644 --- a/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs +++ b/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs @@ -126,6 +126,15 @@ public void UpdateLocalization(LocalizationSettings settings) StateChanged?.Invoke(this, EventArgs.Empty); } + /// + public void UpdateOsRecovery(OsRecoverySettings settings) + { + ArgumentNullException.ThrowIfNull(settings); + Current = Current with { OsRecovery = settings }; + Save(); + StateChanged?.Invoke(this, EventArgs.Empty); + } + /// public void UpdateOperatingSystemSelection(OperatingSystemSelectionSettings settings) { diff --git a/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs b/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs index 861cebcf..2538e6b4 100644 --- a/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs +++ b/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs @@ -97,6 +97,12 @@ public interface IFoundryConfigurationStateService /// New localization settings. void UpdateLocalization(LocalizationSettings settings); + /// + /// Replaces the OS recovery configuration section. + /// + /// New OS recovery settings. + void UpdateOsRecovery(OsRecoverySettings settings); + /// /// Replaces the customization configuration section. /// diff --git a/src/Foundry/Strings/ar-SA/Resources.resw b/src/Foundry/Strings/ar-SA/Resources.resw index 93b71751..4daa010f 100644 --- a/src/Foundry/Strings/ar-SA/Resources.resw +++ b/src/Foundry/Strings/ar-SA/Resources.resw @@ -2121,4 +2121,61 @@ راجع الجاهزية واختر إخراج ISO أو USB ثم ابدأ إنشاء الوسائط. + + تكوين توفر استرداد نظام التشغيل. + + + استرداد نظام التشغيل + + + استرداد نظام التشغيل + + + استرداد نظام التشغيل + + + قم بتمكين نقطة دخول WinRE لـ Foundry Recovery وراجع كيفية وصول المشغلين إليها. + + + تمكين استرداد نظام التشغيل + + + قم بإضافة Foundry Recovery كإجراء استرداد اختياري ضمن بيئة الإصلاح في Windows. + + + تمكين استرداد نظام التشغيل + + + لماذا هو موجود + + + يوفر استرداد نظام التشغيل لفرق الدعم مسار WinRE مألوفًا لبدء تشغيل Foundry Recovery عندما يتعذر على Windows التمهيد بشكل طبيعي أو يحتاج إلى إعادة نشر تعتمد على الإصلاح. + + + متى تستخدمه + + + استخدمه لسير عمل إصلاح الأعطال حيث يبدأ المشغل من بيئة الاسترداد في Windows بدلاً من تشغيل وسائط نشر منفصلة. + + + مسار WinRE + + + استكشاف الأخطاء وإصلاحها > الخيارات المتقدمة > Foundry Recovery + + + القيود + + + تتحكم هذه الصفحة فقط فيما إذا كانت نقطة إدخال الاسترداد مطلوبة أم لا. ولا يقوم بتخصيص نص القائمة، أو تكوين الاسترداد غير المراقب، أو توفير مصادقة الشبكة، أو تسجيل Autopilot، أو إدخال بيانات OA3، أو تخزين الأسرار. + + + تدفق الانتعاش + + + تُظهر صورة العنصر النائب مسار التنقل WinRE المقصود ويمكن استبدالها لاحقًا. + + + مسار التنقل في WinRE: استكشاف الأخطاء وإصلاحها، الخيارات المتقدمة، استرداد Foundry. + diff --git a/src/Foundry/Strings/bg-BG/Resources.resw b/src/Foundry/Strings/bg-BG/Resources.resw index bffc5dfa..b97cc6fe 100644 --- a/src/Foundry/Strings/bg-BG/Resources.resw +++ b/src/Foundry/Strings/bg-BG/Resources.resw @@ -2121,4 +2121,61 @@ Прегледайте готовността, изберете ISO или USB изход и стартирайте създаването на носител. + + Конфигурирайте наличността на възстановяване на ОС. + + + Възстановяване на ОС + + + Възстановяване на ОС + + + Възстановяване на ОС + + + Активирайте входна точка на WinRE за Foundry Recovery и прегледайте как операторите достигат до нея. + + + Активиране на възстановяването на ОС + + + Добавете Foundry Recovery като незадължително действие за възстановяване в Windows Recovery Environment. + + + Активиране на възстановяването на ОС + + + Защо съществува + + + Възстановяването на ОС предоставя на екипите за поддръжка познат път на WinRE за стартиране на Foundry Recovery, когато Windows не може да стартира нормално или се нуждае от повторно разполагане, насочено към ремонт. + + + Кога да го използвате + + + Използвайте го за работни потоци за коригиране на прекъсвания, при които оператор стартира от Windows Recovery Environment, вместо да зарежда отделен носител за разполагане. + + + WinRE път + + + Отстраняване на неизправности > Разширени опции > Foundry Recovery + + + Ограничения + + + Тази страница контролира само дали се изисква входна точка за възстановяване. Той не персонализира текста на менюто, не конфигурира автоматично възстановяване, осигурява удостоверяване на мрежата, регистрира Autopilot, инжектира OA3 данни или съхранява тайни. + + + Поток за възстановяване + + + Изображението на контейнера показва предвидения път за навигация на WinRE и може да бъде заменено по-късно. + + + Път за навигация на WinRE: Отстраняване на неизправности, Разширени опции, Foundry Recovery. + diff --git a/src/Foundry/Strings/cs-CZ/Resources.resw b/src/Foundry/Strings/cs-CZ/Resources.resw index 170553ae..222c1e5d 100644 --- a/src/Foundry/Strings/cs-CZ/Resources.resw +++ b/src/Foundry/Strings/cs-CZ/Resources.resw @@ -2121,4 +2121,61 @@ Zkontrolujte připravenost, vyberte výstup ISO nebo USB a spusťte vytvoření média. + + Nakonfigurujte dostupnost obnovení OS. + + + Obnova OS + + + Obnova OS + + + Obnova OS + + + Povolte vstupní bod WinRE pro Foundry Recovery a zkontrolujte, jak jej operátoři dosáhnou. + + + Povolit obnovení OS + + + Přidejte Foundry Recovery jako volitelnou akci obnovení do prostředí Windows Recovery Environment. + + + Povolit obnovení OS + + + Proč existuje + + + OS Recovery poskytuje týmům podpory známou cestu WinRE pro spuštění Foundry Recovery, když Windows nelze normálně spustit nebo potřebuje přemístění řízené opravou. + + + Kdy ji použít + + + Použijte jej pro pracovní postupy s opravou přerušení, kdy operátor začíná z prostředí Windows Recovery Environment namísto zavádění samostatného média pro nasazení. + + + Cesta WinRE + + + Odstraňování problémů > Pokročilé možnosti > Foundry Recovery + + + Omezení + + + Tato stránka řídí pouze to, zda je požadován vstupní bod obnovení. Nepřizpůsobuje text nabídky, nekonfiguruje bezobslužnou obnovu, zajišťuje síťové ověřování, nezařazuje Autopilot, vkládá data OA3 ani neukládá tajemství. + + + Tok obnovy + + + Zástupný obrázek ukazuje zamýšlenou navigační cestu WinRE a může být později nahrazen. + + + Navigační cesta WinRE: Odstraňování problémů, Pokročilé možnosti, Foundry Recovery. + diff --git a/src/Foundry/Strings/da-DK/Resources.resw b/src/Foundry/Strings/da-DK/Resources.resw index 97df87a9..13d3789b 100644 --- a/src/Foundry/Strings/da-DK/Resources.resw +++ b/src/Foundry/Strings/da-DK/Resources.resw @@ -2121,4 +2121,61 @@ Gennemgå klarheden, vælg ISO- eller USB-output, og start medieoprettelsen. + + Konfigurer OS Gendannelse tilgængelighed. + + + OS Gendannelse + + + OS Gendannelse + + + OS Gendannelse + + + Aktiver et WinRE-indgangspunkt for Foundry Recovery og se, hvordan operatører når det. + + + Aktiver OS-gendannelse + + + Tilføj Foundry Recovery som en valgfri gendannelseshandling under Windows Recovery Environment. + + + Aktiver OS-gendannelse + + + Hvorfor det eksisterer + + + OS Recovery giver supportteams en velkendt WinRE-sti til at starte Foundry Recovery, når Windows ikke kan starte normalt eller har brug for reparationsdrevet omplacering. + + + Hvornår skal man bruge det + + + Brug det til break-fix-arbejdsgange, hvor en operatør starter fra Windows Recovery Environment i stedet for at starte separate installationsmedier. + + + WinRE-sti + + + Fejlfinding > Avancerede indstillinger > Foundry Recovery + + + Begrænsninger + + + Denne side kontrollerer kun, om gendannelsesindgangspunktet anmodes om. Den tilpasser ikke menutekst, konfigurerer uovervåget gendannelse, leverer netværksgodkendelse, tilmelder Autopilot, injicerer OA3-data eller gemmer hemmeligheder. + + + Genopretningsflow + + + Pladsholderbilledet viser den tilsigtede WinRE-navigationssti og kan erstattes senere. + + + WinRE navigationssti: Fejlfinding, Avancerede indstillinger, Foundry Recovery. + diff --git a/src/Foundry/Strings/de-DE/Resources.resw b/src/Foundry/Strings/de-DE/Resources.resw index fa318ea6..27befe93 100644 --- a/src/Foundry/Strings/de-DE/Resources.resw +++ b/src/Foundry/Strings/de-DE/Resources.resw @@ -2121,4 +2121,61 @@ Prüfen Sie die Bereitschaft, wählen Sie eine ISO- oder USB-Ausgabe aus, und starten Sie die Medienerstellung. + + Konfigurieren Sie die Verfügbarkeit der Betriebssystemwiederherstellung. + + + Betriebssystemwiederherstellung + + + Betriebssystemwiederherstellung + + + Betriebssystemwiederherstellung + + + Aktivieren Sie einen WinRE-Einstiegspunkt für Foundry Recovery und überprüfen Sie, wie Bediener ihn erreichen. + + + Aktivieren Sie die Betriebssystemwiederherstellung + + + Fügen Sie Foundry Recovery als optionale Wiederherstellungsaktion unter der Windows-Wiederherstellungsumgebung hinzu. + + + Aktivieren Sie die Betriebssystemwiederherstellung + + + Warum es existiert + + + OS Recovery bietet Supportteams einen vertrauten WinRE-Pfad zum Starten von Foundry Recovery, wenn Windows nicht normal booten kann oder eine reparaturgesteuerte Neubereitstellung benötigt. + + + Wann sollte man es verwenden? + + + Verwenden Sie es für Break-Fix-Workflows, bei denen ein Bediener aus der Windows-Wiederherstellungsumgebung startet, anstatt separate Bereitstellungsmedien zu starten. + + + WinRE-Pfad + + + Fehlerbehebung > Erweiterte Optionen > Foundry Recovery + + + Einschränkungen + + + Diese Seite steuert nur, ob der Wiederherstellungseinstiegspunkt angefordert wird. Es passt keinen Menütext an, konfiguriert keine unbeaufsichtigte Wiederherstellung, stellt keine Netzwerkauthentifizierung bereit, registriert keinen Autopilot, fügt keine OA3-Daten ein und speichert keine Geheimnisse. + + + Erholungsfluss + + + Das Platzhalterbild zeigt den vorgesehenen WinRE-Navigationspfad und kann später ersetzt werden. + + + WinRE-Navigationspfad: Fehlerbehebung, Erweiterte Optionen, Foundry Recovery. + diff --git a/src/Foundry/Strings/el-GR/Resources.resw b/src/Foundry/Strings/el-GR/Resources.resw index 6705494c..89ae28b4 100644 --- a/src/Foundry/Strings/el-GR/Resources.resw +++ b/src/Foundry/Strings/el-GR/Resources.resw @@ -2121,4 +2121,61 @@ Ελέγξτε την ετοιμότητα, επιλέξτε έξοδο ISO ή USB και ξεκινήστε τη δημιουργία μέσου. + + Διαμόρφωση διαθεσιμότητας OS Recovery. + + + Ανάκτηση λειτουργικού συστήματος + + + Ανάκτηση λειτουργικού συστήματος + + + Ανάκτηση λειτουργικού συστήματος + + + Ενεργοποιήστε ένα σημείο εισόδου WinRE για το Foundry Recovery και ελέγξτε πώς το προσεγγίζουν οι χειριστές. + + + Ενεργοποιήστε την ανάκτηση λειτουργικού συστήματος + + + Προσθέστε το Foundry Recovery ως προαιρετική ενέργεια ανάκτησης στο Windows Recovery Environment. + + + Ενεργοποιήστε την ανάκτηση λειτουργικού συστήματος + + + Γιατί υπάρχει + + + Το OS Recovery παρέχει στις ομάδες υποστήριξης μια οικεία διαδρομή WinRE για την εκκίνηση του Foundry Recovery όταν τα Windows δεν μπορούν να εκκινήσουν κανονικά ή χρειάζονται αναδιάταξη βάσει επισκευής. + + + Πότε να το χρησιμοποιήσετε + + + Χρησιμοποιήστε το για ροές εργασιών επιδιόρθωσης διακοπής, όπου ένας χειριστής ξεκινά από το περιβάλλον αποκατάστασης των Windows αντί να εκκινεί ξεχωριστά μέσα ανάπτυξης. + + + Διαδρομή WinRE + + + Αντιμετώπιση προβλημάτων > Προηγμένες επιλογές > Foundry Recovery + + + Περιορισμοί + + + Αυτή η σελίδα ελέγχει μόνο εάν ζητείται το σημείο εισόδου ανάκτησης. Δεν προσαρμόζει το κείμενο μενού, δεν διαμορφώνει την ανάκτηση χωρίς παρακολούθηση, δεν παρέχει έλεγχο ταυτότητας δικτύου, εγγράφει τον Autopilot, δεν εισάγει δεδομένα OA3 ή αποθηκεύει μυστικά. + + + Ροή ανάκτησης + + + Η εικόνα κράτησης θέσης δείχνει την προβλεπόμενη διαδρομή πλοήγησης WinRE και μπορεί να αντικατασταθεί αργότερα. + + + Διαδρομή πλοήγησης WinRE: Αντιμετώπιση προβλημάτων, Προηγμένες επιλογές, Ανάκτηση Foundry. + diff --git a/src/Foundry/Strings/en-GB/Resources.resw b/src/Foundry/Strings/en-GB/Resources.resw index 164a54ac..c302af3b 100644 --- a/src/Foundry/Strings/en-GB/Resources.resw +++ b/src/Foundry/Strings/en-GB/Resources.resw @@ -2121,4 +2121,61 @@ Review readiness, choose ISO or USB output, and start media creation. + + Configure OS Recovery availability. + + + OS Recovery + + + OS Recovery + + + OS Recovery + + + Enable a WinRE entry point for Foundry Recovery and review how operators reach it. + + + Enable OS Recovery + + + Add Foundry Recovery as an optional recovery action under Windows Recovery Environment. + + + Enable OS Recovery + + + Why it exists + + + OS Recovery gives support teams a familiar WinRE path for launching Foundry Recovery when Windows cannot boot normally or needs repair-driven redeployment. + + + When to use it + + + Use it for break-fix workflows where an operator starts from Windows Recovery Environment instead of booting separate deployment media. + + + WinRE path + + + Troubleshoot > Advanced options > Foundry Recovery + + + Limitations + + + This page only controls whether the recovery entry point is requested. It does not customize menu text, configure unattended recovery, provision network authentication, enroll Autopilot, inject OA3 data, or store secrets. + + + Recovery flow + + + The placeholder image shows the intended WinRE navigation path and can be replaced later. + + + WinRE navigation path: Troubleshoot, Advanced options, Foundry Recovery. + diff --git a/src/Foundry/Strings/en-US/Resources.resw b/src/Foundry/Strings/en-US/Resources.resw index 44858eb6..80923b02 100644 --- a/src/Foundry/Strings/en-US/Resources.resw +++ b/src/Foundry/Strings/en-US/Resources.resw @@ -2121,4 +2121,61 @@ Review readiness, choose ISO or USB output, and start media creation. + + Configure OS Recovery availability. + + + OS Recovery + + + OS Recovery + + + OS Recovery + + + Enable a WinRE entry point for Foundry Recovery and review how operators reach it. + + + Enable OS Recovery + + + Add Foundry Recovery as an optional recovery action under Windows Recovery Environment. + + + Enable OS Recovery + + + Why it exists + + + OS Recovery gives support teams a familiar WinRE path for launching Foundry Recovery when Windows cannot boot normally or needs repair-driven redeployment. + + + When to use it + + + Use it for break-fix workflows where an operator starts from Windows Recovery Environment instead of booting separate deployment media. + + + WinRE path + + + Troubleshoot > Advanced options > Foundry Recovery + + + Limitations + + + This page only controls whether the recovery entry point is requested. It does not customize menu text, configure unattended recovery, provision network authentication, enroll Autopilot, inject OA3 data, or store secrets. + + + Recovery flow + + + The placeholder image shows the intended WinRE navigation path and can be replaced later. + + + WinRE navigation path: Troubleshoot, Advanced options, Foundry Recovery. + diff --git a/src/Foundry/Strings/es-ES/Resources.resw b/src/Foundry/Strings/es-ES/Resources.resw index d967dec6..5359666e 100644 --- a/src/Foundry/Strings/es-ES/Resources.resw +++ b/src/Foundry/Strings/es-ES/Resources.resw @@ -2121,4 +2121,61 @@ Revise la preparación, elija una salida ISO o USB e inicie la creación del medio. + + Configure la disponibilidad de recuperación del sistema operativo. + + + Recuperación del sistema operativo + + + Recuperación del sistema operativo + + + Recuperación del sistema operativo + + + Habilite un punto de entrada de WinRE para Foundry Recovery y revise cómo los operadores llegan a él. + + + Habilitar la recuperación del sistema operativo + + + Agregue Foundry Recovery como una acción de recuperación opcional en el entorno de recuperación de Windows. + + + Habilitar la recuperación del sistema operativo + + + Por que existe + + + OS Recovery brinda a los equipos de soporte una ruta familiar de WinRE para iniciar Foundry Recovery cuando Windows no puede iniciarse normalmente o necesita una reimplementación impulsada por reparación. + + + Cuando usarlo + + + Úselo para flujos de trabajo de reparación de averías en los que un operador inicia desde el entorno de recuperación de Windows en lugar de iniciar medios de implementación separados. + + + Ruta de WinRE + + + Solucionar problemas > Opciones avanzadas > Recuperación de Foundry + + + Limitaciones + + + Esta página solo controla si se solicita el punto de entrada de recuperación. No personaliza el texto del menú, no configura la recuperación desatendida, no proporciona autenticación de red, no registra el Autopilot, no inyecta datos OA3 ni almacena secretos. + + + Flujo de recuperación + + + La imagen del marcador de posición muestra la ruta de navegación prevista de WinRE y se puede reemplazar más tarde. + + + Ruta de navegación de WinRE: solución de problemas, opciones avanzadas, recuperación de Foundry. + diff --git a/src/Foundry/Strings/es-MX/Resources.resw b/src/Foundry/Strings/es-MX/Resources.resw index 74ca2682..a2c039ef 100644 --- a/src/Foundry/Strings/es-MX/Resources.resw +++ b/src/Foundry/Strings/es-MX/Resources.resw @@ -2121,4 +2121,61 @@ Revise la preparación, elija una salida ISO o USB e inicie la creación del medio. + + Configure la disponibilidad de recuperación del sistema operativo. + + + Recuperación del sistema operativo + + + Recuperación del sistema operativo + + + Recuperación del sistema operativo + + + Habilite un punto de entrada de WinRE para Foundry Recovery y revise cómo los operadores llegan a él. + + + Habilitar la recuperación del sistema operativo + + + Agregue Foundry Recovery como una acción de recuperación opcional en el entorno de recuperación de Windows. + + + Habilitar la recuperación del sistema operativo + + + Por que existe + + + OS Recovery brinda a los equipos de soporte una ruta familiar de WinRE para iniciar Foundry Recovery cuando Windows no puede iniciarse normalmente o necesita una reimplementación impulsada por reparación. + + + Cuando usarlo + + + Úselo para flujos de trabajo de reparación de averías en los que un operador inicia desde el entorno de recuperación de Windows en lugar de iniciar medios de implementación separados. + + + Ruta de WinRE + + + Solucionar problemas > Opciones avanzadas > Recuperación de Foundry + + + Limitaciones + + + Esta página solo controla si se solicita el punto de entrada de recuperación. No personaliza el texto del menú, no configura la recuperación desatendida, no proporciona autenticación de red, no registra el Autopilot, no inyecta datos OA3 ni almacena secretos. + + + Flujo de recuperación + + + La imagen del marcador de posición muestra la ruta de navegación prevista de WinRE y se puede reemplazar más tarde. + + + Ruta de navegación de WinRE: solución de problemas, opciones avanzadas, recuperación de Foundry. + diff --git a/src/Foundry/Strings/et-EE/Resources.resw b/src/Foundry/Strings/et-EE/Resources.resw index a6fd5417..814485ee 100644 --- a/src/Foundry/Strings/et-EE/Resources.resw +++ b/src/Foundry/Strings/et-EE/Resources.resw @@ -2121,4 +2121,61 @@ Kontrollige valmisolekut, valige ISO või USB väljund ja alustage meediumi loomist. + + OS-i taastamise kättesaadavuse seadistamine. + + + OS-i taastamine + + + OS-i taastamine + + + OS-i taastamine + + + Lubage Foundry Recovery jaoks WinRE sisenemispunkt ja vaadake, kuidas operaatorid selleni jõuavad. + + + Luba OS-i taastamine + + + Lisage Foundry Recovery valikulise taastetoiminguna Windowsi taastekeskkonna alla. + + + Luba OS-i taastamine + + + Miks see eksisteerib + + + OS-i taastamine annab tugimeeskondadele tuttava WinRE-tee Foundry Recovery käivitamiseks, kui Windows ei saa normaalselt käivitada või vajab remondipõhist ümberpaigutamist. + + + Millal seda kasutada + + + Kasutage seda katkestuste parandamise töövoogude jaoks, kus operaator alustab Windowsi taastekeskkonnast, selle asemel et käivitada eraldi juurutuskandjat. + + + WinRE tee + + + Veaotsing > Täpsemad valikud > Foundry Recovery + + + Piirangud + + + See leht juhib ainult seda, kas taastamise sisestuspunkti taotletakse. See ei kohanda menüüteksti, konfigureeri järelevalveta taastamist, võrgu autentimist, Autopilot registreerimist, OA3-andmete sisestamist ega saladuste salvestamist. + + + Taastumise voog + + + Kohatäite pilt näitab kavandatud WinRE navigatsiooniteed ja selle saab hiljem asendada. + + + WinRE navigeerimistee: tõrkeotsing, täpsemad valikud, Foundry Recovery. + diff --git a/src/Foundry/Strings/fi-FI/Resources.resw b/src/Foundry/Strings/fi-FI/Resources.resw index 986f7c23..2aff869f 100644 --- a/src/Foundry/Strings/fi-FI/Resources.resw +++ b/src/Foundry/Strings/fi-FI/Resources.resw @@ -2121,4 +2121,61 @@ Tarkista valmius, valitse ISO- tai USB-tuloste ja käynnistä median luonti. + + Määritä käyttöjärjestelmän palautuksen saatavuus. + + + Käyttöjärjestelmän palautus + + + Käyttöjärjestelmän palautus + + + Käyttöjärjestelmän palautus + + + Ota WinRE-tulopiste käyttöön Foundry Recoveryssa ja tarkista, kuinka operaattorit saavuttavat sen. + + + Ota käyttöjärjestelmän palautus käyttöön + + + Lisää Foundry Recovery valinnaiseksi palautustoiminnoksi Windowsin palautusympäristöön. + + + Ota käyttöjärjestelmän palautus käyttöön + + + Miksi se on olemassa + + + OS Recovery tarjoaa tukiryhmille tutun WinRE-polun Foundry Recovery -ohjelman käynnistämiseen, kun Windows ei voi käynnistyä normaalisti tai tarvitsee korjaukseen perustuvan uudelleenasennuksen. + + + Milloin sitä käytetään + + + Käytä sitä häiriönkorjaustyönkuluissa, joissa operaattori aloittaa Windowsin palautusympäristöstä erillisen käyttöönottotietovälineen käynnistämisen sijaan. + + + WinRE polku + + + Vianmääritys > Lisäasetukset > Foundry Recovery + + + Rajoitukset + + + Tämä sivu hallitsee vain sitä, pyydetäänkö palautuksen aloituspistettä. Se ei mukauta valikon tekstiä, määritä valvomatonta palautusta, tarjoa verkon todennusta, rekisteröi Autopilot, lisää OA3-tietoja tai tallenna salaisuuksia. + + + Palautusvirtaus + + + Paikkamerkkikuvassa näkyy aiottu WinRE-navigointipolku, ja se voidaan korvata myöhemmin. + + + WinRE-navigointipolku: Vianmääritys, Lisäasetukset, Foundry Recovery. + diff --git a/src/Foundry/Strings/fr-CA/Resources.resw b/src/Foundry/Strings/fr-CA/Resources.resw index ed20a236..162ec59e 100644 --- a/src/Foundry/Strings/fr-CA/Resources.resw +++ b/src/Foundry/Strings/fr-CA/Resources.resw @@ -28,7 +28,7 @@ Chargement des contributeurs... - Contributions: {0} + Contributions : {0} Foundry OSD est une application moderne permettant de créer des supports de déploiement Windows optimisés par WinPE. @@ -1522,7 +1522,7 @@ Norme WinPE - WinRE Wi-Fi image + Image WinRE Wi-Fi Dell @@ -2121,4 +2121,61 @@ Vérifiez l’état, choisissez une sortie ISO ou USB, puis lancez la création du support. + + Configurer la disponibilité de la récupération du SE. + + + Récupération du SE + + + Récupération du SE + + + Récupération du SE + + + Activer un point d’entrée WinRE pour Foundry Recovery et vérifier comment les opérateurs y accèdent. + + + Activer la récupération du SE + + + Ajoute Foundry Recovery comme action de récupération facultative dans Windows Recovery Environment. + + + Activer la récupération du SE + + + Pourquoi cela existe + + + La récupération du SE donne aux équipes de soutien un chemin WinRE familier pour lancer Foundry Recovery lorsque Windows ne peut pas démarrer normalement ou nécessite un redéploiement de réparation. + + + Quand l’utiliser + + + Utilisez-la pour les scénarios de dépannage où un opérateur démarre depuis Windows Recovery Environment au lieu de démarrer un support de déploiement séparé. + + + Chemin WinRE + + + Dépannage > Options avancées > Foundry Recovery + + + Limites + + + Cette page contrôle uniquement si le point d’entrée de récupération est demandé. Elle ne personnalise pas le texte du menu, ne configure pas la récupération sans assistance, ne provisionne pas l’authentification réseau, n’inscrit pas Autopilot, n’injecte pas de données OA3 et ne stocke pas de secrets. + + + Flux de récupération + + + L’image placeholder montre le chemin de navigation WinRE prévu et pourra être remplacée plus tard. + + + Chemin de navigation WinRE : Dépannage, Options avancées, Foundry Recovery. + diff --git a/src/Foundry/Strings/fr-FR/Resources.resw b/src/Foundry/Strings/fr-FR/Resources.resw index 55985ad4..9d0373e6 100644 --- a/src/Foundry/Strings/fr-FR/Resources.resw +++ b/src/Foundry/Strings/fr-FR/Resources.resw @@ -2121,4 +2121,61 @@ Vérifiez la disponibilité, choisissez une sortie ISO ou USB, puis démarrez la création du média. + + Configurer la disponibilité de la récupération de l’OS. + + + Récupération de l’OS + + + Récupération de l’OS + + + Récupération de l’OS + + + Activer un point d’entrée WinRE pour Foundry Recovery et vérifier comment les opérateurs y accèdent. + + + Activer la récupération de l’OS + + + Ajoute Foundry Recovery comme action de récupération facultative dans Windows Recovery Environment. + + + Activer la récupération de l’OS + + + Pourquoi cela existe + + + La récupération de l’OS donne aux équipes de support un chemin WinRE familier pour lancer Foundry Recovery lorsque Windows ne peut pas démarrer normalement ou nécessite un redéploiement de réparation. + + + Quand l’utiliser + + + Utilisez-la pour les scénarios de dépannage où un opérateur démarre depuis Windows Recovery Environment au lieu de démarrer un support de déploiement séparé. + + + Chemin WinRE + + + Dépannage > Options avancées > Foundry Recovery + + + Limites + + + Cette page contrôle uniquement si le point d’entrée de récupération est demandé. Elle ne personnalise pas le texte du menu, ne configure pas la récupération sans assistance, ne provisionne pas l’authentification réseau, n’inscrit pas Autopilot, n’injecte pas de données OA3 et ne stocke pas de secrets. + + + Flux de récupération + + + L’image placeholder montre le chemin de navigation WinRE prévu et pourra être remplacée plus tard. + + + Chemin de navigation WinRE : Dépannage, Options avancées, Foundry Recovery. + diff --git a/src/Foundry/Strings/he-IL/Resources.resw b/src/Foundry/Strings/he-IL/Resources.resw index b71148bf..bc62a1f9 100644 --- a/src/Foundry/Strings/he-IL/Resources.resw +++ b/src/Foundry/Strings/he-IL/Resources.resw @@ -2121,4 +2121,61 @@ בדוק מוכנות, בחר פלט ISO או USB והתחל ביצירת המדיה. + + הגדר את זמינות שחזור מערכת ההפעלה. + + + שחזור מערכת הפעלה + + + שחזור מערכת הפעלה + + + שחזור מערכת הפעלה + + + אפשר נקודת כניסה של WinRE עבור Foundry Recovery וסקור כיצד מפעילים מגיעים אליה. + + + אפשר שחזור מערכת הפעלה + + + הוסף Foundry Recovery כפעולת שחזור אופציונלית תחת סביבת השחזור של Windows. + + + אפשר שחזור מערכת הפעלה + + + למה זה קיים + + + OS Recovery נותן לצוותי תמיכה נתיב WinRE מוכר להפעלת Foundry Recovery כאשר Windows אינו יכול לאתחל כרגיל או זקוק לפריסה מחדש מונעת תיקון. + + + מתי להשתמש בו + + + השתמש בו עבור זרימות עבודה של תיקון הפסקות שבהן מפעיל מתחיל מסביבת השחזור של Windows במקום לאתחל מדיית פריסה נפרדת. + + + נתיב WinRE + + + פתרון בעיות > אפשרויות מתקדמות > Foundry Recovery + + + מגבלות + + + דף זה שולט רק אם נקודת הכניסה לשחזור מתבקשת. הוא אינו מבצע התאמה אישית של טקסט התפריט, מגדיר שחזור ללא השגחה, מתן אימות רשת, רושם Autopilot, מזרים נתוני OA3 או מאחסן סודות. + + + זרימת התאוששות + + + תמונת מציין המיקום מציגה את נתיב הניווט המיועד של WinRE וניתן להחליפה מאוחר יותר. + + + נתיב ניווט WinRE: פתרון בעיות, אפשרויות מתקדמות, Foundry Recovery. + diff --git a/src/Foundry/Strings/hr-HR/Resources.resw b/src/Foundry/Strings/hr-HR/Resources.resw index 16173f1c..87e88adf 100644 --- a/src/Foundry/Strings/hr-HR/Resources.resw +++ b/src/Foundry/Strings/hr-HR/Resources.resw @@ -2121,4 +2121,61 @@ Pregledajte spremnost, odaberite ISO ili USB izlaz i pokrenite stvaranje medija. + + Konfigurirajte dostupnost OS Recovery. + + + Oporavak OS-a + + + Oporavak OS-a + + + Oporavak OS-a + + + Omogućite WinRE ulaznu točku za Foundry Recovery i pregledajte kako operateri dolaze do nje. + + + Omogući oporavak OS-a + + + Dodajte Foundry Recovery kao opcionalnu radnju oporavka u Windows Recovery Environment. + + + Omogući oporavak OS-a + + + Zašto postoji + + + OS Recovery daje timovima za podršku poznati WinRE put za pokretanje programa Foundry Recovery kada se Windows ne može normalno pokrenuti ili mu je potrebna ponovna implementacija na temelju popravka. + + + Kada ga koristiti + + + Koristite ga za radne tijekove popravljanja prijeloma gdje operater počinje iz okruženja za oporavak sustava Windows umjesto pokretanja zasebnog medija za implementaciju. + + + WinRE put + + + Rješavanje problema > Napredne opcije > Foundry Recovery + + + Ograničenja + + + Ova stranica kontrolira samo je li zatražena ulazna točka oporavka. Ne prilagođava tekst izbornika, konfigurira oporavak bez nadzora, pruža mrežnu provjeru autentičnosti, upisuje Autopilot, ubacuje OA3 podatke ili pohranjuje tajne. + + + Tijek oporavka + + + Slika rezerviranog mjesta prikazuje predviđenu navigacijsku putanju WinRE i može se zamijeniti kasnije. + + + WinRE navigacijski put: Rješavanje problema, Napredne opcije, Foundry Recovery. + diff --git a/src/Foundry/Strings/hu-HU/Resources.resw b/src/Foundry/Strings/hu-HU/Resources.resw index f7242cc0..63a6029b 100644 --- a/src/Foundry/Strings/hu-HU/Resources.resw +++ b/src/Foundry/Strings/hu-HU/Resources.resw @@ -2121,4 +2121,61 @@ Tekintse át a készenlétet, válasszon ISO vagy USB kimenetet, majd indítsa el az adathordozó létrehozását. + + Konfigurálja az operációs rendszer helyreállításának elérhetőségét. + + + Operációs rendszer helyreállítása + + + Operációs rendszer helyreállítása + + + Operációs rendszer helyreállítása + + + Engedélyezze a WinRE belépési pontot a Foundry Recovery számára, és tekintse át, hogyan érik el az üzemeltetők. + + + Operációs rendszer helyreállításának engedélyezése + + + A Foundry Recovery hozzáadása opcionális helyreállítási műveletként a Windows helyreállítási környezetben. + + + Operációs rendszer helyreállításának engedélyezése + + + Miért létezik? + + + Az operációs rendszer helyreállítása ismerős WinRE elérési utat biztosít a támogató csapatok számára a Foundry Recovery elindításához, amikor a Windows nem tud normál módon elindulni, vagy javítási célú átcsoportosításra van szükség. + + + Mikor kell használni? + + + Használja olyan hibaelhárítási munkafolyamatokhoz, ahol az operátor a Windows helyreállítási környezetből indul, ahelyett, hogy külön telepítési adathordozót indítana el. + + + WinRE elérési út + + + Hibaelhárítás > Speciális beállítások > Foundry Recovery + + + Korlátozások + + + Ez az oldal csak azt vezérli, hogy a helyreállítási belépési pont szükséges-e. Nem szabja testre a menü szövegét, nem konfigurálja a felügyelet nélküli helyreállítást, nem biztosítja a hálózati hitelesítést, nem regisztrálja az Autopilot, nem adja be az OA3-adatokat, és nem tárol titkokat. + + + Visszanyerési áram + + + A helyőrző kép a kívánt WinRE navigációs útvonalat mutatja, és később cserélhető. + + + WinRE navigációs útvonal: Hibaelhárítás, Speciális beállítások, Foundry Recovery. + diff --git a/src/Foundry/Strings/it-IT/Resources.resw b/src/Foundry/Strings/it-IT/Resources.resw index ae11c654..5949ab45 100644 --- a/src/Foundry/Strings/it-IT/Resources.resw +++ b/src/Foundry/Strings/it-IT/Resources.resw @@ -2121,4 +2121,61 @@ Verifica la preparazione, scegli un output ISO o USB e avvia la creazione del supporto. + + Configurare la disponibilità del ripristino del sistema operativo. + + + Ripristino OS + + + Ripristino OS + + + Ripristino OS + + + Abilita un punto di ingresso WinRE per Foundry Recovery ed esamina il modo in cui gli operatori lo raggiungono. + + + Abilita ripristino del sistema operativo + + + Aggiungere Foundry Recovery come azione di ripristino opzionale in Ambiente ripristino Windows. + + + Abilita ripristino del sistema operativo + + + Perché esiste + + + OS Recovery offre ai team di supporto un percorso WinRE familiare per avviare Foundry Recovery quando Windows non può avviarsi normalmente o necessita di una ridistribuzione basata sulla riparazione. + + + Quando usarlo + + + Utilizzarlo per flussi di lavoro break-fix in cui un operatore inizia da Ambiente ripristino Windows invece di avviare supporti di distribuzione separati. + + + Percorso WinRE + + + Risoluzione dei problemi > Opzioni avanzate > Foundry Recovery + + + Limitazioni + + + Questa pagina controlla solo se è richiesto il punto di ingresso di ripristino. Non personalizza il testo del menu, configura il ripristino automatico, esegue il provisioning dell'autenticazione di rete, registra il Autopilot, inietta dati OA3 o memorizza segreti. + + + Flusso di recupero + + + L'immagine segnaposto mostra il percorso di navigazione WinRE previsto e può essere sostituita in seguito. + + + Percorso di navigazione WinRE: risoluzione dei problemi, opzioni avanzate, Foundry Recovery. + diff --git a/src/Foundry/Strings/ja-JP/Resources.resw b/src/Foundry/Strings/ja-JP/Resources.resw index 07e07dca..dd61722f 100644 --- a/src/Foundry/Strings/ja-JP/Resources.resw +++ b/src/Foundry/Strings/ja-JP/Resources.resw @@ -2121,4 +2121,61 @@ 準備状況を確認し、ISO または USB 出力を選択して、メディアの作成を開始します。 + + OSリカバリの可用性を設定します。 + + + OSリカバリ + + + OSリカバリ + + + OSリカバリ + + + Foundry RecoveryのWinREエントリーポイントを有効にし、オペレーターがどのように到達するかを確認します。 + + + OSリカバリを有効にする + + + Windowsリカバリ環境で、オプションのリカバリアクションとしてFoundryリカバリを追加します。 + + + OSリカバリを有効にする + + + なぜ存在するのか + + + OSリカバリは、Windowsが正常に起動できない場合、または修理主導の再展開が必要な場合に、サポートチームがFoundryリカバリを起動するための使い慣れたWinREパスを提供します。 + + + 使用するタイミング + + + 別の展開メディアを起動する代わりに、オペレータがWindowsリカバリ環境から開始するブレークフィックスワークフローに使用します。 + + + WinREパス + + + トラブルシューティング>高度なオプション> Foundry Recovery + + + 制限事項 + + + このページでは、リカバリエントリポイントが要求されるかどうかのみを制御します。 メニューテキストをカスタマイズしたり、無人リカバリを設定したり、ネットワーク認証をプロビジョニングしたり、Autopilotを登録したり、OA 3データを挿入したり、シークレットを保存したりすることはありません。 + + + 回復フロー + + + プレースホルダー画像は、意図されたWinREナビゲーションパスを示し、後で置き換えることができます。 + + + WinREナビゲーションパス:トラブルシューティング、高度なオプション、Foundryリカバリ。 + diff --git a/src/Foundry/Strings/ko-KR/Resources.resw b/src/Foundry/Strings/ko-KR/Resources.resw index aeaed98b..52b6210b 100644 --- a/src/Foundry/Strings/ko-KR/Resources.resw +++ b/src/Foundry/Strings/ko-KR/Resources.resw @@ -2121,4 +2121,88 @@ 준비 상태를 검토하고 ISO 또는 USB 출력을 선택한 다음 미디어 만들기를 시작합니다. + + @ +OS 복구 가용성을 구성합니다. +@ + + + @ +OS 복구 +@ + + + @ +OS 복구 + + + @ +OS 복구 +@ + + + @ +Foundry Recovery에 대한 WinRE 진입점을 활성화하고 운영자가 도달하는 방법을 검토합니다. +@ + + + @ +OS 복구 활성화 + + + @ +Windows 복구 환경에서 선택적 복구 작업으로 Foundry Recovery를 추가합니다. +@ + + + @ +OS 복구 활성화 +@ + + + @ +숙소가 존재하는 이유 + + + @ +OS 복구는 Windows가 정상적으로 부팅되지 않거나 수리 중심의 재배포가 필요한 경우 지원 팀에게 Foundry Recovery를 시작할 수 있는 친숙한 WinRE 경로를 제공합니다. +@ + + + @ +사용 시기 +@ + + + @ +운영자가 별도의 배포 미디어를 부팅하는 대신 Windows 복구 환경에서 시작하는 브레이크 픽스 워크플로우에 사용합니다. + + + @ +WinRE 경로 +@ + + + 문제 해결 > 고급 옵션 > Foundry Recovery + + + @ +제한 사항 + + + @ +이 페이지는 복구 진입점 요청 여부만 제어합니다. 메뉴 텍스트를 사용자 정의하거나, 무인 복구를 구성하거나, 네트워크 인증을 프로비저닝하거나, Autopilot을 등록하거나, OA3 데이터를 주입하거나, 비밀을 저장하지 않습니다. + + + @ +복구 흐름 +@ + + + @ +자리 표시자 이미지는 의도된 WinRE 탐색 경로를 보여주며 나중에 교체할 수 있습니다. + + + WinRE 탐색 경로: 문제 해결, 고급 옵션, Foundry Recovery. + diff --git a/src/Foundry/Strings/lt-LT/Resources.resw b/src/Foundry/Strings/lt-LT/Resources.resw index c4bc7669..95065a90 100644 --- a/src/Foundry/Strings/lt-LT/Resources.resw +++ b/src/Foundry/Strings/lt-LT/Resources.resw @@ -2121,4 +2121,61 @@ Peržiūrėkite parengtį, pasirinkite ISO arba USB išvestį ir pradėkite laikmenos kūrimą. + + Konfigūruokite OS atkūrimo prieinamumą. + + + OS atkūrimas + + + OS atkūrimas + + + OS atkūrimas + + + Įgalinkite Foundry Recovery WinRE įėjimo tašką ir peržiūrėkite, kaip operatoriai jį pasiekia. + + + Įgalinti OS atkūrimą + + + Pridėkite Foundry Recovery kaip pasirenkamą atkūrimo veiksmą „Windows“ atkūrimo aplinkoje. + + + Įgalinti OS atkūrimą + + + Kodėl taip yra + + + „OS Recovery“ suteikia palaikymo komandoms įprastą „WinRE“ kelią paleisti „Foundry Recovery“, kai „Windows“ negali normaliai paleisti arba reikia atlikti taisomąjį pakartotinį diegimą. + + + Kada jį naudoti + + + Naudokite jį „break-fix“ darbo eigoms, kai operatorius paleidžiamas iš „Windows“ atkūrimo aplinkos, o ne įkelia atskiras diegimo laikmenas. + + + WinRE kelias + + + Trikčių šalinimas > Išplėstinės parinktys > Foundry Recovery + + + Apribojimai + + + Šis puslapis kontroliuoja tik tai, ar prašoma atkūrimo įėjimo taško. Ji nepritaiko meniu teksto, nekonfigūruoja neprižiūrimo atkūrimo, neteikia tinklo autentifikavimo, neužregistruoja Autopilot, neinjektuoja OA3 duomenų ar nesaugo paslapčių. + + + Atkūrimo srautas + + + Vietos rezervavimo ženklo paveikslėlyje rodomas numatytas WinRE naršymo kelias, kurį vėliau galima pakeisti. + + + WinRE naršymo kelias: trikčių šalinimas, išplėstinės parinktys, Foundry Recovery. + diff --git a/src/Foundry/Strings/lv-LV/Resources.resw b/src/Foundry/Strings/lv-LV/Resources.resw index 95c22aad..c7240c92 100644 --- a/src/Foundry/Strings/lv-LV/Resources.resw +++ b/src/Foundry/Strings/lv-LV/Resources.resw @@ -1966,7 +1966,7 @@ latviešu (Latvija) - Norwegian Bokmal (Norway) + norvēģu bukmols (Norvēģija) holandiešu (Nīderlande) @@ -2121,4 +2121,61 @@ Pārskatiet gatavību, izvēlieties ISO vai USB izvadi un sāciet datu nesēja izveidi. + + Konfigurējiet OS atkopšanas pieejamību. + + + OS atkopšana + + + OS atkopšana + + + OS atkopšana + + + Iespējojiet WinRE ieejas punktu Foundry Recovery un pārskatiet, kā operatori to sasniedz. + + + Iespējot OS atkopšanu + + + Pievieno Foundry Recovery kā neobligātu atkopšanas darbību Windows Recovery Environment vidē. + + + Iespējot OS atkopšanu + + + Kādēļ tas pastāv + + + OS atkopšana sniedz atbalsta komandām pazīstamu WinRE ceļu Foundry Recovery palaišanai, ja Windows nevar normāli sāknēt vai nepieciešama labošanas pārizvietošana. + + + Kad to lietot + + + Izmantojiet to labojumfailu darbplūsmām, kurās operators startē no Windows atkopšanas vides, nevis sāknē atsevišķu izvietošanas datu nesēju. + + + WinRE ceļš + + + Problēmu novēršana > Papildu opcijas > Foundry Recovery + + + Ierobežojumi + + + Šī lapa kontrolē tikai to, vai ir pieprasīts atkopšanas ieejas punkts. Tas nepielāgo izvēlnes tekstu, nekonfigurē neuzraudzītu atkopšanu, nenodrošina tīkla autentifikāciju, nereģistrē Autopilot, neievada OA3 datus vai neglabā noslēpumus. + + + Reģenerācijas plūsma + + + Viettura attēlā ir redzams paredzētais WinRE navigācijas ceļš, un to var aizstāt vēlāk. + + + WinRE navigācijas ceļš: Problēmu novēršana, Papildu opcijas, Foundry Recovery. + diff --git a/src/Foundry/Strings/nb-NO/Resources.resw b/src/Foundry/Strings/nb-NO/Resources.resw index ceaf3fe1..0e244c3b 100644 --- a/src/Foundry/Strings/nb-NO/Resources.resw +++ b/src/Foundry/Strings/nb-NO/Resources.resw @@ -2121,4 +2121,61 @@ Kontroller beredskapen, velg ISO- eller USB-utdata, og start medieopprettingen. + + Konfigurer tilgjengelighet for OS-gjenoppretting. + + + OS-gjenoppretting + + + OS-gjenoppretting + + + OS-gjenoppretting + + + Aktiver et WinRE-inngangspunkt for Foundry Recovery og se hvordan operatører når det. + + + Aktiver OS-gjenoppretting + + + Legg til Foundry Recovery som en valgfri gjenopprettingshandling under Windows Recovery Environment. + + + Aktiver OS-gjenoppretting + + + Hvorfor den eksisterer + + + OS Recovery gir støtteteam en kjent WinRE-bane for å starte Foundry Recovery når Windows ikke kan starte opp normalt eller trenger reparasjonsdrevet omdistribuering. + + + Når du skal bruke den + + + Bruk den til break-fix-arbeidsflyter der en operatør starter fra Windows-gjenopprettingsmiljøet i stedet for å starte opp separate distribusjonsmedier. + + + WinRE-bane + + + Feilsøking > Avanserte alternativer > Foundry Recovery + + + Begrensninger + + + Denne siden kontrollerer bare om gjenopprettingsinngangspunktet er forespurt. Den tilpasser ikke menytekst, konfigurerer uovervåket gjenoppretting, klargjør nettverksautentisering, registrerer Autopilot, injiserer OA3-data eller lagrer hemmeligheter. + + + Gjenopprettingsflyt + + + Plassholderbildet viser den tiltenkte WinRE-navigasjonsbanen og kan erstattes senere. + + + WinRE-navigasjonssti: Feilsøking, Avanserte alternativer, Foundry Recovery. + diff --git a/src/Foundry/Strings/nl-NL/Resources.resw b/src/Foundry/Strings/nl-NL/Resources.resw index f554f03a..57f89e3d 100644 --- a/src/Foundry/Strings/nl-NL/Resources.resw +++ b/src/Foundry/Strings/nl-NL/Resources.resw @@ -1711,7 +1711,7 @@ Updaten en opnieuw opstarten - Update Foundry OSD + Foundry OSD bijwerken De update downloaden. Foundry OSD zal opnieuw opstarten als het klaar is. @@ -2121,4 +2121,61 @@ Controleer de gereedheid, kies ISO- of USB-uitvoer en start het maken van media. + + Beschikbaarheid van OS-herstel configureren. + + + OS-herstel + + + OS-herstel + + + OS-herstel + + + Schakel een WinRE-ingangspunt in voor Foundry Recovery en bekijk hoe operators dit bereiken. + + + OS-herstel inschakelen + + + Voeg Foundry Recovery toe als een optionele herstelactie onder Windows Recovery Environment. + + + OS-herstel inschakelen + + + Waarom het bestaat + + + OS Recovery biedt ondersteuningsteams een bekend WinRE-pad voor het starten van Foundry Recovery wanneer Windows niet normaal kan opstarten of een reparatiegestuurde herschikking nodig heeft. + + + Wanneer te gebruiken + + + Gebruik het voor break-fix workflows waarbij een operator start vanuit Windows Recovery Environment in plaats van afzonderlijke implementatiemedia op te starten. + + + WinRE-pad + + + Problemen oplossen > Geavanceerde opties > Foundry Recovery + + + Beperkingen + + + Deze pagina bepaalt alleen of het herstelinvoerpunt wordt aangevraagd. Het past de menutekst niet aan, configureert geen onbeheerd herstel, voorziet niet in netwerkverificatie, schrijft Autopilot niet in, injecteert geen OA3-gegevens en slaat geen geheimen op. + + + Herstelstroom + + + De tijdelijke aanduiding toont het beoogde WinRE-navigatiepad en kan later worden vervangen. + + + WinRE-navigatiepad: problemen oplossen, geavanceerde opties, Foundry Recovery. + diff --git a/src/Foundry/Strings/pl-PL/Resources.resw b/src/Foundry/Strings/pl-PL/Resources.resw index 22ed23e2..adb61af2 100644 --- a/src/Foundry/Strings/pl-PL/Resources.resw +++ b/src/Foundry/Strings/pl-PL/Resources.resw @@ -2121,4 +2121,61 @@ Sprawdź gotowość, wybierz wyjście ISO lub USB i rozpocznij tworzenie nośnika. + + Skonfiguruj dostępność odzyskiwania systemu operacyjnego. + + + Odzyskiwanie systemu operacyjnego + + + Odzyskiwanie systemu operacyjnego + + + Odzyskiwanie systemu operacyjnego + + + Włącz punkt wejścia WinRE dla Foundry Recovery i sprawdź, jak operatorzy do niego docierają. + + + Włącz odzyskiwanie systemu operacyjnego + + + Dodaj Foundry Recovery jako opcjonalną akcję odzyskiwania w środowisku odzyskiwania systemu Windows. + + + Włącz odzyskiwanie systemu operacyjnego + + + Dlaczego istnieje + + + System operacyjny Recovery zapewnia zespołom wsparcia znaną ścieżkę WinRE do uruchamiania Foundry Recovery, gdy system Windows nie może się normalnie uruchomić lub wymaga ponownego uruchomienia w celu naprawy. + + + Kiedy go używać + + + Użyj go do procedur naprawczych, w których operator zaczyna od środowiska odzyskiwania systemu Windows, zamiast uruchamiać oddzielny nośnik wdrażania. + + + Ścieżka WinRE + + + Rozwiązywanie problemów > Opcje zaawansowane > Foundry Recovery + + + Ograniczenia + + + Ta strona kontroluje tylko, czy żądany jest punkt wejścia odzyskiwania. Nie dostosowuje tekstu menu, nie konfiguruje nienadzorowanego odzyskiwania, nie zapewnia uwierzytelniania sieciowego, nie rejestruje Autopilot, nie wstrzykuje danych OA3 ani nie przechowuje tajemnic. + + + Przepływ odzysku + + + Obraz zastępczy pokazuje zamierzoną ścieżkę nawigacji WinRE i może zostać zastąpiony później. + + + Ścieżka nawigacji WinRE: Rozwiązywanie problemów, Opcje zaawansowane, Foundry Recovery. + diff --git a/src/Foundry/Strings/pt-BR/Resources.resw b/src/Foundry/Strings/pt-BR/Resources.resw index f394cc34..e3cfc80a 100644 --- a/src/Foundry/Strings/pt-BR/Resources.resw +++ b/src/Foundry/Strings/pt-BR/Resources.resw @@ -2121,4 +2121,61 @@ Revise a prontidão, escolha uma saída ISO ou USB e inicie a criação da mídia. + + Configurar a disponibilidade da Recuperação do SO. + + + Recuperação de SO + + + Recuperação de SO + + + Recuperação de SO + + + Habilite um ponto de entrada WinRE para Foundry Recovery e analise como os operadores o alcançam. + + + Ativar recuperação do sistema operacional + + + Adicione o Foundry Recovery como uma ação de recuperação opcional no Ambiente de Recuperação do Windows. + + + Ativar recuperação do sistema operacional + + + Por que existe + + + A recuperação do sistema operacional oferece às equipes de suporte um caminho familiar do WinRE para iniciar a recuperação do Foundry quando o Windows não consegue inicializar normalmente ou precisa de redistribuição orientada por reparo. + + + Quando usar + + + Use-o para fluxos de trabalho de correção de avarias em que um operador inicia a partir do Ambiente de Recuperação do Windows em vez de inicializar a mídia de implantação separada. + + + Caminho WinRE + + + Solução de problemas > Opções avançadas > Foundry Recovery + + + Limitações + + + Esta página controla apenas se o ponto de entrada de recuperação é solicitado. Ele não personaliza o texto do menu, configura a recuperação autônoma, fornece autenticação de rede, registra o Autopilot, injeta dados OA3 ou armazena segredos. + + + Fluxo de recuperação + + + A imagem do espaço reservado mostra o caminho de navegação do WinRE pretendido e pode ser substituída posteriormente. + + + Caminho de navegação do WinRE: Solução de problemas, Opções avançadas, Recuperação do Foundry. + diff --git a/src/Foundry/Strings/pt-PT/Resources.resw b/src/Foundry/Strings/pt-PT/Resources.resw index 1202a058..a71e61ae 100644 --- a/src/Foundry/Strings/pt-PT/Resources.resw +++ b/src/Foundry/Strings/pt-PT/Resources.resw @@ -2121,4 +2121,61 @@ Reveja a preparação, escolha uma saída ISO ou USB e inicie a criação do suporte. + + Configure a disponibilidade do OS Recovery. + + + Recuperação do sistema operacional + + + Recuperação do sistema operacional + + + Recuperação do sistema operacional + + + Habilite um ponto de entrada WinRE para Foundry Recovery e analise como os operadores o alcançam. + + + Habilitar recuperação do sistema operacional + + + Adicione Foundry Recovery como uma ação de recuperação opcional no Ambiente de Recuperação do Windows. + + + Habilitar recuperação do sistema operacional + + + Por que existe + + + O OS Recovery oferece às equipes de suporte um caminho familiar do WinRE para iniciar o Foundry Recovery quando o Windows não consegue inicializar normalmente ou precisa de uma reimplantação orientada por reparo. + + + Quando usar + + + Use-o para fluxos de trabalho de correção em que um operador inicia no Ambiente de Recuperação do Windows em vez de inicializar uma mídia de implantação separada. + + + Caminho WinRE + + + Solução de problemas > Opções avançadas > Foundry Recovery + + + Limitações + + + Esta página controla apenas se o ponto de entrada de recuperação é solicitado. Ele não personaliza o texto do menu, configura recuperação autônoma, provisiona autenticação de rede, registra o Autopilot, injeta dados OA3 ou armazena segredos. + + + Fluxo de recuperação + + + A imagem do espaço reservado mostra o caminho de navegação pretendido do WinRE e pode ser substituída posteriormente. + + + Caminho de navegação do WinRE: Solução de problemas, Opções avançadas, Foundry Recovery. + diff --git a/src/Foundry/Strings/ro-RO/Resources.resw b/src/Foundry/Strings/ro-RO/Resources.resw index f81a8def..2edd561f 100644 --- a/src/Foundry/Strings/ro-RO/Resources.resw +++ b/src/Foundry/Strings/ro-RO/Resources.resw @@ -2121,4 +2121,61 @@ Revizuiți starea, alegeți ieșirea ISO sau USB și porniți crearea suportului. + + Configurați disponibilitatea recuperării OS. + + + Recuperarea sistemului de operare + + + Recuperarea sistemului de operare + + + Recuperarea sistemului de operare + + + Activați un punct de intrare WinRE pentru Foundry Recovery și examinați modul în care operatorii îl ajung. + + + Activați recuperarea sistemului de operare + + + Adăugați Foundry Recovery ca acțiune opțională de recuperare în Windows Recovery Environment. + + + Activați recuperarea sistemului de operare + + + De ce există + + + OS Recovery oferă echipelor de asistență o cale WinRE familiară pentru lansarea Foundry Recovery atunci când Windows nu poate porni normal sau are nevoie de o redistribuire bazată pe reparații. + + + Când să-l folosești + + + Folosiți-l pentru fluxuri de lucru pentru reparații în cazul în care un operator pornește din mediul de recuperare Windows în loc să pornească medii de implementare separate. + + + Calea WinRE + + + Depanare > Opțiuni avansate > Foundry Recovery + + + Limitări + + + Această pagină controlează doar dacă este solicitat punctul de intrare de recuperare. Nu personalizează textul meniului, nu configurează recuperarea nesupravegheată, nu asigură autentificarea rețelei, nu înregistrează Autopilot, nu injectează date OA3 sau nu stochează secrete. + + + Fluxul de recuperare + + + Imaginea substituent arată calea de navigare WinRE intenționată și poate fi înlocuită ulterior. + + + Cale de navigare WinRE: Depanare, Opțiuni avansate, Recuperare Foundry. + diff --git a/src/Foundry/Strings/ru-RU/Resources.resw b/src/Foundry/Strings/ru-RU/Resources.resw index 0f39066b..42ad1319 100644 --- a/src/Foundry/Strings/ru-RU/Resources.resw +++ b/src/Foundry/Strings/ru-RU/Resources.resw @@ -2121,4 +2121,61 @@ Проверьте готовность, выберите вывод ISO или USB и запустите создание носителя. + + Настройте доступность восстановления ОС. + + + Восстановление ОС + + + Восстановление ОС + + + Восстановление ОС + + + Включите точку входа WinRE для Foundry Recovery и проверьте, как операторы достигают ее. + + + Включить восстановление ОС + + + Добавьте Foundry Recovery в качестве дополнительного действия по восстановлению в среде восстановления Windows. + + + Включить восстановление ОС + + + Почему это существует + + + OS Recovery предоставляет группам поддержки знакомый путь WinRE для запуска Foundry Recovery, когда Windows не может нормально загрузиться или требуется повторное развертывание после восстановления. + + + Когда его использовать + + + Используйте его для рабочих процессов устранения неполадок, в которых оператор запускает среду восстановления Windows вместо загрузки отдельного носителя развертывания. + + + Путь WinRE + + + Устранение неполадок > Дополнительные параметры > Foundry Recovery + + + Ограничения + + + Эта страница контролирует только то, запрашивается ли точка входа для восстановления. Он не настраивает текст меню, не настраивает автоматическое восстановление, не обеспечивает сетевую аутентификацию, не регистрирует Autopilot, не вводит данные OA3 и не хранит секреты. + + + Поток восстановления + + + Изображение-заполнитель показывает предполагаемый путь навигации WinRE и может быть заменено позже. + + + Путь навигации WinRE: Устранение неполадок, Дополнительные параметры, Foundry Recovery. + diff --git a/src/Foundry/Strings/sk-SK/Resources.resw b/src/Foundry/Strings/sk-SK/Resources.resw index df2129fc..dfc124a7 100644 --- a/src/Foundry/Strings/sk-SK/Resources.resw +++ b/src/Foundry/Strings/sk-SK/Resources.resw @@ -1441,7 +1441,7 @@ Nahrávanie hardvérového hash je povolené, ale vybraný PFX nezodpovedá registračnému certifikátu aplikácie. - Hardware hash upload is enabled but the selected certificate thumbprint is missing. + Nahrávanie hardvérového hashu je povolené, ale chýba odtlačok vybratého certifikátu. Nahrávanie hardvérového hash je povolené, ale chýba vybratý certifikát. @@ -1450,22 +1450,22 @@ Nahrávanie hardvérového hash je povolené, ale platnosť vybratého certifikátu vypršala. Pred vytvorením zavádzacieho média vyberte platný certifikát. - Hardware hash upload is enabled but no boot media PFX is selected. + Nahrávanie hardvérového hashu je povolené, ale nie je vybratý žiadny PFX pre zavádzacie médium. Odovzdávanie hardvérového hash je povolené, ale chýba heslo PFX zavádzacieho média. - Hardware hash upload is enabled but the boot media PFX has not been validated. + Nahrávanie hardvérového hashu je povolené, ale PFX zavádzacieho média nebol overený. - Hardware hash upload is enabled but the selected PFX does not match the active certificate. + Nahrávanie hardvérového hashu je povolené, ale vybratý PFX sa nezhoduje s aktívnym certifikátom. Odovzdávanie hardvérového hash je povolené, ale nebolo možné overiť platnosť spúšťacieho média PFX. - Hardware hash upload is enabled but the selected boot media PFX has expired. + Nahrávanie hardvérového hashu je povolené, ale vybratému PFX zavádzacieho média vypršala platnosť. Doplnok ADK alebo WinPE nie je pripravený. @@ -1477,7 +1477,7 @@ WinPE jazyk zavádzania nie je vybratý. - Selected WinPE boot language is not available for the selected architecture. + Vybraný jazyk zavádzania WinPE nie je dostupný pre vybranú architektúru. Konfigurácia siete nie je pripravená. @@ -1507,7 +1507,7 @@ Vlastný priečinok ovládača neexistuje. - Custom driver folder does not contain .inf files. + Vlastný priečinok ovládačov neobsahuje súbory .inf. Vytváranie médií nie je v tejto zostave povolené. @@ -1639,31 +1639,31 @@ Nastavenia - Manage startup, language, diagnostics, and application behavior. + Spravujte spustenie, jazyk, diagnostiku a správanie aplikácie. generál - Send anonymous usage telemetry to help improve Foundry. No names, secrets, SSIDs, IP addresses, file paths, disk identifiers, computer names, Autopilot profile names, or hardware identifiers are sent. + Odosielajte anonymnú telemetriu používania, ktorá pomáha zlepšovať Foundry. Neodosielajú sa žiadne mená, tajomstvá, SSID, IP adresy, cesty k súborom, identifikátory diskov, názvy počítačov, názvy profilov Autopilot ani identifikátory hardvéru. Povoliť telemetriu - Change theme, backdrop, and accent color preferences. + Zmeňte motív, pozadie a predvoľby farby zvýraznenia. Vzhľad - Check for Foundry OSD updates and review release notes. + Vyhľadajte aktualizácie Foundry OSD a pozrite si poznámky k vydaniu. Aktualizovať aplikáciu - Open Windows color settings to change the accent color. + Otvorte nastavenia farieb systému Windows a zmeňte farbu zvýraznenia. Farba akcentu @@ -2121,4 +2121,61 @@ Skontrolujte pripravenosť, vyberte výstup ISO alebo USB a spustite vytvorenie média. + + Nakonfigurujte dostupnosť obnovy OS. + + + Obnova OS + + + Obnova OS + + + Obnova OS + + + Povoľte vstupný bod WinRE pre Foundry Recovery a skontrolujte, ako ho operátori dosiahnu. + + + Povoliť obnovenie OS + + + Pridajte Foundry Recovery ako voliteľnú akciu obnovenia do prostredia Windows Recovery Environment. + + + Povoliť obnovenie OS + + + Prečo existuje + + + OS Recovery poskytuje tímom podpory známu cestu WinRE na spustenie Foundry Recovery, keď sa systém Windows nemôže normálne zaviesť alebo potrebuje opätovné nasadenie po opravách. + + + Kedy ho použiť + + + Použite ho na pracovné toky opráv prerušenia, kde operátor začína z prostredia Windows Recovery Environment namiesto zavádzania samostatného média nasadenia. + + + Cesta WinRE + + + Riešenie problémov > Rozšírené možnosti > Foundry Recovery + + + Obmedzenia + + + Táto stránka kontroluje iba to, či sa požaduje vstupný bod obnovy. Neprispôsobuje text ponuky, nekonfiguruje bezobslužnú obnovu, neposkytuje sieťovú autentifikáciu, nezaregistruje Autopilot, nevkladá údaje OA3 ani neukladá tajomstvá. + + + Tok obnovy + + + Zástupný obrázok zobrazuje zamýšľanú navigačnú cestu WinRE a môže byť neskôr nahradený. + + + Navigačná cesta WinRE: Riešenie problémov, Rozšírené možnosti, Foundry Recovery. + diff --git a/src/Foundry/Strings/sl-SI/Resources.resw b/src/Foundry/Strings/sl-SI/Resources.resw index 2ace6c4f..6b1e428c 100644 --- a/src/Foundry/Strings/sl-SI/Resources.resw +++ b/src/Foundry/Strings/sl-SI/Resources.resw @@ -2121,4 +2121,61 @@ Preverite pripravljenost, izberite izhod ISO ali USB in začnite ustvarjanje medija. + + Konfigurirajte razpoložljivost obnovitve OS. + + + Obnovitev OS + + + Obnovitev OS + + + Obnovitev OS + + + Omogočite vstopno točko WinRE za Foundry Recovery in preglejte, kako jo operaterji dosežejo. + + + Omogoči obnovitev OS + + + Dodajte Foundry Recovery kot neobvezno obnovitveno dejanje v obnovitvenem okolju Windows. + + + Omogoči obnovitev OS + + + Zakaj obstaja + + + OS Recovery nudi skupinam za podporo znano pot WinRE za zagon Foundry Recovery, ko se Windows ne more normalno zagnati ali potrebuje ponovno namestitev, ki temelji na popravilu. + + + Kdaj ga uporabiti + + + Uporabite ga za delovne poteke popravkov prekinitev, kjer operater začne iz obnovitvenega okolja Windows, namesto da bi zagnal ločene medije za uvajanje. + + + WinRE pot + + + Odpravljanje težav > Napredne možnosti > Foundry Recovery + + + Omejitve + + + Ta stran samo nadzira, ali je zahtevana obnovitvena vstopna točka. Ne prilagaja besedila menija, konfigurira nenadzorovano obnovitev, omogoča preverjanje pristnosti omrežja, vpisuje Autopilot, vnaša podatke OA3 ali shranjuje skrivnosti. + + + Tok okrevanja + + + Slika nadomestnega znaka prikazuje predvideno navigacijsko pot WinRE in jo je mogoče pozneje zamenjati. + + + Navigacijska pot WinRE: Odpravljanje težav, Napredne možnosti, Foundry Recovery. + diff --git a/src/Foundry/Strings/sr-Latn-RS/Resources.resw b/src/Foundry/Strings/sr-Latn-RS/Resources.resw index 100e0ed2..fb778d02 100644 --- a/src/Foundry/Strings/sr-Latn-RS/Resources.resw +++ b/src/Foundry/Strings/sr-Latn-RS/Resources.resw @@ -2121,4 +2121,61 @@ Pregledajte spremnost, izaberite ISO ili USB izlaz i pokrenite kreiranje medija. + + Konfigurišite dostupnost oporavka OS-a. + + + Oporavak OS-a + + + Oporavak OS-a + + + Oporavak OS-a + + + Omogućite WinRE ulaznu tačku za Foundry Recovery i pregledajte kako joj operateri pristupaju. + + + Omogući oporavak OS-a + + + Dodaje Foundry Recovery kao opcionalnu radnju oporavka u Windows Recovery Environment. + + + Omogući oporavak OS-a + + + Zašto postoji + + + Oporavak OS-a daje timovima podrške poznatu WinRE putanju za pokretanje Foundry Recovery kada Windows ne može normalno da se pokrene ili zahteva ponovno postavljanje radi popravke. + + + Kada ga koristiti + + + Koristite ga za tokove popravke u kojima operater počinje iz Windows Recovery Environment umesto pokretanja zasebnog medija za implementaciju. + + + WinRE putanja + + + Rešavanje problema > Napredne opcije > Foundry Recovery + + + Ograničenja + + + Ova stranica kontroliše samo da li se zahteva ulazna tačka oporavka. Ne prilagođava tekst menija, ne konfiguriše nenadzirani oporavak, ne obezbeđuje mrežnu autentifikaciju, ne upisuje Autopilot, ne ubacuje OA3 podatke i ne čuva tajne. + + + Tok oporavka + + + Slika rezervisanog mesta prikazuje planiranu WinRE navigacionu putanju i može se kasnije zameniti. + + + WinRE navigaciona putanja: Rešavanje problema, Napredne opcije, Foundry Recovery. + diff --git a/src/Foundry/Strings/sv-SE/Resources.resw b/src/Foundry/Strings/sv-SE/Resources.resw index d2897c88..2dd1b2cf 100644 --- a/src/Foundry/Strings/sv-SE/Resources.resw +++ b/src/Foundry/Strings/sv-SE/Resources.resw @@ -64,7 +64,7 @@ Laddar versionsinformation... - Release notes + Versionsinformation Förvar @@ -94,7 +94,7 @@ Uppdatera och starta om - Release notes + Versionsinformation Uppdatera källa @@ -2121,4 +2121,61 @@ Granska beredskapen, välj ISO- eller USB-utdata och starta medieskapandet. + + Konfigurera OS-återställningstillgänglighet. + + + OS-återställning + + + OS-återställning + + + OS-återställning + + + Aktivera en WinRE-ingångspunkt för Foundry Recovery och granska hur operatörerna når den. + + + Aktivera OS-återställning + + + Lägg till Foundry Recovery som en valfri återställningsåtgärd under Windows Recovery Environment. + + + Aktivera OS-återställning + + + Varför det finns + + + OS Recovery ger supportteam en välbekant WinRE-sökväg för att starta Foundry Recovery när Windows inte kan starta normalt eller behöver reparationsdriven omdistribuering. + + + När ska man använda den + + + Använd den för break-fix-arbetsflöden där en operatör startar från Windows Recovery Environment istället för att starta upp separata distributionsmedia. + + + WinRE sökväg + + + Felsökning > Avancerade alternativ > Foundry Recovery + + + Begränsningar + + + Den här sidan styr bara om återställningsingångspunkten begärs. Den anpassar inte menytext, konfigurerar oövervakad återställning, tillhandahåller nätverksautentisering, registrerar Autopilot, injicerar OA3-data eller lagrar hemligheter. + + + Återhämtningsflöde + + + Platshållarbilden visar den avsedda WinRE-navigeringsvägen och kan ersättas senare. + + + WinRE-navigeringsväg: Felsökning, Avancerade alternativ, Foundry Recovery. + diff --git a/src/Foundry/Strings/th-TH/Resources.resw b/src/Foundry/Strings/th-TH/Resources.resw index a20bf90f..e869f094 100644 --- a/src/Foundry/Strings/th-TH/Resources.resw +++ b/src/Foundry/Strings/th-TH/Resources.resw @@ -2121,4 +2121,61 @@ ตรวจสอบความพร้อม เลือกเอาต์พุต ISO หรือ USB แล้วเริ่มสร้างสื่อ + + กำหนดค่าความพร้อมใช้งานของการกู้คืนระบบปฏิบัติการ + + + การกู้คืนระบบปฏิบัติการ + + + การกู้คืนระบบปฏิบัติการ + + + การกู้คืนระบบปฏิบัติการ + + + เปิดใช้งานจุดเข้าใช้งาน WinRE สำหรับ Foundry Recovery และตรวจสอบว่าผู้ปฏิบัติงานเข้าถึงจุดนั้นได้อย่างไร + + + เปิดใช้งานการกู้คืนระบบปฏิบัติการ + + + เพิ่ม Foundry Recovery เป็นการดำเนินการกู้คืนเสริมภายใต้ Windows Recovery Environment + + + เปิดใช้งานการกู้คืนระบบปฏิบัติการ + + + ทำไมมันถึงมีอยู่ + + + OS Recovery ช่วยให้ทีมสนับสนุนมีเส้นทาง WinRE ที่คุ้นเคยในการเปิดใช้ Foundry Recovery เมื่อ Windows ไม่สามารถบูตได้ตามปกติหรือจำเป็นต้องปรับใช้การซ่อมแซมใหม่ + + + เมื่อใดจึงจะใช้มัน + + + ใช้สำหรับเวิร์กโฟลว์การแก้ไขที่ผู้ปฏิบัติงานเริ่มต้นจาก Windows Recovery Environment แทนที่จะบูตสื่อการปรับใช้แยกต่างหาก + + + เส้นทาง WinRE + + + แก้ไขปัญหา > ตัวเลือกขั้นสูง > การกู้คืน Foundry + + + ข้อจำกัด + + + หน้านี้ควบคุมเฉพาะว่าจะขอจุดเข้ากู้คืนหรือไม่ ไม่ได้ปรับแต่งข้อความเมนู กำหนดค่าการกู้คืนแบบอัตโนมัติ จัดเตรียมการตรวจสอบสิทธิ์เครือข่าย ลงทะเบียน Autopilot แทรกข้อมูล OA3 หรือเก็บข้อมูลลับ + + + กระแสการกู้คืน + + + รูปภาพตัวยึดแสดงเส้นทางการนำทาง WinRE ที่ต้องการและสามารถแทนที่ได้ในภายหลัง + + + เส้นทางการนำทาง WinRE: การแก้ไขปัญหา, ตัวเลือกขั้นสูง, การกู้คืน Foundry + diff --git a/src/Foundry/Strings/tr-TR/Resources.resw b/src/Foundry/Strings/tr-TR/Resources.resw index d1eda324..b2937556 100644 --- a/src/Foundry/Strings/tr-TR/Resources.resw +++ b/src/Foundry/Strings/tr-TR/Resources.resw @@ -2121,4 +2121,61 @@ Hazırlığı gözden geçirin, ISO veya USB çıktısını seçin ve medya oluşturmayı başlatın. + + İşletim Sistemi Kurtarma kullanılabilirliğini yapılandırın. + + + İşletim Sistemi Kurtarma + + + İşletim Sistemi Kurtarma + + + İşletim Sistemi Kurtarma + + + Foundry Recovery için bir WinRE giriş noktasını etkinleştirin ve operatörlerin bu noktaya nasıl ulaştığını inceleyin. + + + İşletim Sistemi Kurtarmayı Etkinleştir + + + Windows Kurtarma Ortamı altında Foundry Recovery'yi isteğe bağlı bir kurtarma eylemi olarak ekleyin. + + + İşletim Sistemi Kurtarmayı Etkinleştir + + + Neden var? + + + İşletim Sistemi Kurtarma, Windows'un normal şekilde önyükleme yapamadığı veya onarım odaklı yeniden dağıtıma ihtiyaç duyduğu durumlarda, destek ekiplerine Foundry Recovery'yi başlatmak için tanıdık bir WinRE yolu sağlar. + + + Ne zaman kullanılmalı? + + + Operatörün ayrı dağıtım ortamını önyüklemek yerine Windows Kurtarma Ortamı'ndan başlattığı arıza düzeltme iş akışları için bunu kullanın. + + + WinRE yolu + + + Sorun giderme > Gelişmiş seçenekler > FOUNDRY_RECOVERY_product + + + Sınırlamalar + + + Bu sayfa yalnızca kurtarma giriş noktasının istenip istenmediğini kontrol eder. Menü metnini özelleştirmez, gözetimsiz kurtarmayı yapılandırmaz, ağ kimlik doğrulamasını sağlamaz, Autopilot kaydetmez, OA3 verilerini eklemez veya sırları saklamaz. + + + Kurtarma akışı + + + Yer tutucu görüntüsü amaçlanan WinRE gezinme yolunu gösterir ve daha sonra değiştirilebilir. + + + WinRE gezinme yolu: Sorun giderme, Gelişmiş seçenekler, FOUNDRY_RECOVERY_product. + diff --git a/src/Foundry/Strings/uk-UA/Resources.resw b/src/Foundry/Strings/uk-UA/Resources.resw index 48eacb57..733c811d 100644 --- a/src/Foundry/Strings/uk-UA/Resources.resw +++ b/src/Foundry/Strings/uk-UA/Resources.resw @@ -2121,4 +2121,61 @@ Перевірте готовність, виберіть вихід ISO або USB і запустіть створення носія. + + Налаштувати доступність відновлення ОС. + + + Відновлення ОС + + + Відновлення ОС + + + Відновлення ОС + + + Увімкніть точку входу WinRE для Foundry Recovery і подивіться, як оператори досягають її. + + + Увімкніть відновлення ОС + + + Додайте Foundry Recovery як додаткову дію відновлення в середовищі відновлення Windows. + + + Увімкніть відновлення ОС + + + Чому він існує + + + OS Recovery надає командам підтримки знайомий шлях WinRE для запуску Foundry Recovery, коли Windows не може нормально завантажитися або потребує повторного розгортання з метою відновлення. + + + Коли це використовувати + + + Використовуйте його для робочих процесів усунення поломок, коли оператор починає із середовища відновлення Windows замість завантаження окремого носія для розгортання. + + + Шлях WinRE + + + Усунення несправностей > Додаткові параметри > Foundry Recovery + + + Обмеження + + + Ця сторінка лише визначає, чи запитується точка входу для відновлення. Він не налаштовує текст меню, не налаштовує автоматичне відновлення, автентифікацію мережі, не зареєстровує Autopilot, не вводить дані OA3 і не зберігає секрети. + + + Потік відновлення + + + Зображення заповнювача показує передбачуваний шлях навігації WinRE, і його можна замінити пізніше. + + + Шлях навігації WinRE: усунення несправностей, додаткові параметри, Foundry Recovery. + diff --git a/src/Foundry/Strings/zh-CN/Resources.resw b/src/Foundry/Strings/zh-CN/Resources.resw index 0c616a4b..2704b167 100644 --- a/src/Foundry/Strings/zh-CN/Resources.resw +++ b/src/Foundry/Strings/zh-CN/Resources.resw @@ -2121,4 +2121,61 @@ 检查就绪情况,选择 ISO 或 USB 输出,然后开始创建媒体。 + + 配置操作系统恢复可用性。 + + + 操作系统恢复 + + + 操作系统恢复 + + + 操作系统恢复 + + + 启用 Foundry Recovery 的 WinRE 入口点并查看操作员如何访问它。 + + + 启用操作系统恢复 + + + 将 Foundry 恢复添加为 Windows 恢复环境下的可选恢复操作。 + + + 启用操作系统恢复 + + + 为什么存在 + + + OS Recovery 为支持团队提供了熟悉的 WinRE 路径,用于在 Windows 无法正常启动或需要修复驱动的重新部署时启动 Foundry Recovery。 + + + 何时使用它 + + + 将其用于故障修复工作流程,其中操作员从 Windows 恢复环境启动,而不是启动单独的部署介质。 + + + WinRE路径 + + + 疑难解答 > 高级选项 > Foundry 恢复 + + + 局限性 + + + 该页面仅控制是否请求恢复入口点。它不会自定义菜单文本、配置无人值守恢复、提供网络身份验证、注册 Autopilot、注入 OA3 数据或存储机密。 + + + 回收流程 + + + 占位符图像显示预期的 WinRE 导航路径,并且可以稍后替换。 + + + WinRE 导航路径:故障排除、高级选项、Foundry 恢复。 + diff --git a/src/Foundry/Strings/zh-TW/Resources.resw b/src/Foundry/Strings/zh-TW/Resources.resw index ca9a6261..638e672d 100644 --- a/src/Foundry/Strings/zh-TW/Resources.resw +++ b/src/Foundry/Strings/zh-TW/Resources.resw @@ -2121,4 +2121,61 @@ 檢閱就緒狀態,選擇 ISO 或 USB 輸出,然後開始建立媒體。 + + 配置作業系統恢復可用性。 + + + 作業系統復原 + + + 作業系統復原 + + + 作業系統復原 + + + 啟用 Foundry Recovery 的 WinRE 入口點並查看操作員如何存取它。 + + + 啟用作業系統復原 + + + 將 Foundry 復原新增為 Windows 復原環境下的選用復原作業。 + + + 啟用作業系統復原 + + + 為什麼存在 + + + OS Recovery 為支援團隊提供了熟悉的 WinRE 路徑,可在 Windows 無法正常啟動或需要修復驅動程式的重新部署時啟動 Foundry Recovery。 + + + 何時使用它 + + + 將其用於故障修復工作流程,其中操作員從 Windows 復原環境啟動,而不是啟動單獨的部署媒體。 + + + WinRE路徑 + + + 疑難排解 > 進階選項 > Foundry 恢復 + + + 限制 + + + 此頁面僅控制是否請求恢復入口點。它不會自訂選單文字、配置無人值守恢復、提供網路驗證、註冊 Autopilot、注入 OA3 資料或儲存機密。 + + + 回收流程 + + + 佔位符影像顯示預期的 WinRE 導航路徑,並且可以稍後替換。 + + + WinRE 導覽路徑:故障排除、進階選項、Foundry 復原。 + diff --git a/src/Foundry/ViewModels/OsRecoveryConfigurationViewModel.cs b/src/Foundry/ViewModels/OsRecoveryConfigurationViewModel.cs new file mode 100644 index 00000000..f7815324 --- /dev/null +++ b/src/Foundry/ViewModels/OsRecoveryConfigurationViewModel.cs @@ -0,0 +1,137 @@ +using Foundry.Core.Models.Configuration; +using Foundry.Services.Configuration; +using Foundry.Services.Localization; + +namespace Foundry.ViewModels; + +/// +/// Backs the OS Recovery page with the current authoring toggle and explanatory text. +/// +public sealed partial class OsRecoveryConfigurationViewModel : ObservableObject, IDisposable +{ + private readonly IApplicationLocalizationService localizationService; + private readonly IFoundryConfigurationStateService configurationStateService; + private bool isUpdatingFromState; + + public OsRecoveryConfigurationViewModel( + IApplicationLocalizationService localizationService, + IFoundryConfigurationStateService configurationStateService) + { + this.localizationService = localizationService; + this.configurationStateService = configurationStateService; + RefreshLocalizedText(); + RefreshConfigurationState(); + localizationService.LanguageChanged += OnLanguageChanged; + configurationStateService.StateChanged += OnConfigurationStateChanged; + } + + [ObservableProperty] + public partial string PageTitle { get; set; } = string.Empty; + + [ObservableProperty] + public partial string PageDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string EnableHeader { get; set; } = string.Empty; + + [ObservableProperty] + public partial string EnableDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string EnableToggleText { get; set; } = string.Empty; + + [ObservableProperty] + public partial string WhyHeader { get; set; } = string.Empty; + + [ObservableProperty] + public partial string WhyDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string WhenHeader { get; set; } = string.Empty; + + [ObservableProperty] + public partial string WhenDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string WinRePathHeader { get; set; } = string.Empty; + + [ObservableProperty] + public partial string WinRePathDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string LimitationsHeader { get; set; } = string.Empty; + + [ObservableProperty] + public partial string LimitationsDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string FlowHeader { get; set; } = string.Empty; + + [ObservableProperty] + public partial string FlowDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string FlowImageAutomationName { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool IsOsRecoveryEnabled { get; set; } + + public string DocumentationUrl => FoundryApplicationInfo.OsRecoveryDocumentationUrl; + + /// + public void Dispose() + { + localizationService.LanguageChanged -= OnLanguageChanged; + configurationStateService.StateChanged -= OnConfigurationStateChanged; + } + + private void OnLanguageChanged(object? sender, ApplicationLanguageChangedEventArgs e) + { + RefreshLocalizedText(); + } + + private void OnConfigurationStateChanged(object? sender, EventArgs e) + { + RefreshConfigurationState(); + } + + partial void OnIsOsRecoveryEnabledChanged(bool value) + { + if (isUpdatingFromState) + { + return; + } + + configurationStateService.UpdateOsRecovery(new OsRecoverySettings + { + IsEnabled = value + }); + } + + private void RefreshConfigurationState() + { + isUpdatingFromState = true; + IsOsRecoveryEnabled = configurationStateService.Current.OsRecovery.IsEnabled; + isUpdatingFromState = false; + } + + private void RefreshLocalizedText() + { + PageTitle = localizationService.GetString("OsRecoveryPage_Title.Text"); + PageDescription = localizationService.GetString("OsRecovery.PageDescription"); + EnableHeader = localizationService.GetString("OsRecovery.EnableHeader"); + EnableDescription = localizationService.GetString("OsRecovery.EnableDescription"); + EnableToggleText = localizationService.GetString("OsRecovery.EnableToggleText"); + WhyHeader = localizationService.GetString("OsRecovery.WhyHeader"); + WhyDescription = localizationService.GetString("OsRecovery.WhyDescription"); + WhenHeader = localizationService.GetString("OsRecovery.WhenHeader"); + WhenDescription = localizationService.GetString("OsRecovery.WhenDescription"); + WinRePathHeader = localizationService.GetString("OsRecovery.WinRePathHeader"); + WinRePathDescription = localizationService.GetString("OsRecovery.WinRePathDescription"); + LimitationsHeader = localizationService.GetString("OsRecovery.LimitationsHeader"); + LimitationsDescription = localizationService.GetString("OsRecovery.LimitationsDescription"); + FlowHeader = localizationService.GetString("OsRecovery.FlowHeader"); + FlowDescription = localizationService.GetString("OsRecovery.FlowDescription"); + FlowImageAutomationName = localizationService.GetString("OsRecovery.FlowImageAutomationName"); + } +} diff --git a/src/Foundry/Views/OsRecoveryPage.xaml b/src/Foundry/Views/OsRecoveryPage.xaml new file mode 100644 index 00000000..c7fa8882 --- /dev/null +++ b/src/Foundry/Views/OsRecoveryPage.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Foundry/Views/OsRecoveryPage.xaml.cs b/src/Foundry/Views/OsRecoveryPage.xaml.cs new file mode 100644 index 00000000..c2cb6818 --- /dev/null +++ b/src/Foundry/Views/OsRecoveryPage.xaml.cs @@ -0,0 +1,19 @@ +namespace Foundry.Views; + +public sealed partial class OsRecoveryPage : Page +{ + public OsRecoveryConfigurationViewModel ViewModel { get; } + + public OsRecoveryPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + Unloaded += OnUnloaded; + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Unloaded -= OnUnloaded; + ViewModel.Dispose(); + } +}