diff --git a/src/Foundry.Core.Tests/WinPe/WinPeUsbMediaServiceTests.cs b/src/Foundry.Core.Tests/WinPe/WinPeUsbMediaServiceTests.cs index 2fec8832..a6c92c2f 100644 --- a/src/Foundry.Core.Tests/WinPe/WinPeUsbMediaServiceTests.cs +++ b/src/Foundry.Core.Tests/WinPe/WinPeUsbMediaServiceTests.cs @@ -163,40 +163,98 @@ public void VerifyBootPartitionLayout_WhenBootPartitionIsMinimal_ReturnsSuccess( } [Fact] - public void BuildDiskPartScript_WhenGptQuickFormat_CreatesBootAndCachePartitionsWithoutActive() + public void BuildPowerShellProvisioningScript_WhenGptQuickFormat_CreatesBootAndCachePartitionsWithoutActive() { - IReadOnlyList script = WinPeUsbMediaService.BuildDiskPartScript( + string script = WinPeUsbMediaService.BuildPowerShellProvisioningScript( diskNumber: 7, partitionStyle: UsbPartitionStyle.Gpt, formatMode: UsbFormatMode.Quick, bootDriveLetter: 'S', cacheDriveLetter: 'T'); - Assert.Contains("select disk 7", script); - Assert.Contains("convert mbr noerr", script); - Assert.Contains("convert gpt", script); - Assert.Contains("create partition primary size=4096", script); - Assert.Contains("format fs=fat32 quick label=BOOT", script); - Assert.Contains("format fs=ntfs quick label=\"Foundry Cache\"", script); - Assert.DoesNotContain("active", script); + Assert.Contains("$diskNumber = 7", script, StringComparison.Ordinal); + Assert.Contains("$partitionStyle = 'GPT'", script, StringComparison.Ordinal); + Assert.Contains("Initialize-Disk -Number $diskNumber -PartitionStyle $partitionStyle", script, StringComparison.Ordinal); + Assert.Contains("New-Partition -DiskNumber $diskNumber -Size 4096MB -DriveLetter $bootDriveLetter", script, StringComparison.Ordinal); + Assert.Contains("FileSystem = 'FAT32'", script, StringComparison.Ordinal); + Assert.Contains("NewFileSystemLabel = 'BOOT'", script, StringComparison.Ordinal); + Assert.Contains("New-Partition -DiskNumber $diskNumber -UseMaximumSize -DriveLetter $cacheDriveLetter", script, StringComparison.Ordinal); + Assert.Contains("FileSystem = 'NTFS'", script, StringComparison.Ordinal); + Assert.Contains("NewFileSystemLabel = 'Foundry Cache'", script, StringComparison.Ordinal); + Assert.DoesNotContain("-IsActive $true", script, StringComparison.Ordinal); + Assert.Contains("$fullFormat = $false", script, StringComparison.Ordinal); } [Fact] - public void BuildDiskPartScript_WhenMbrCompleteFormat_MarksBootPartitionActiveWithoutQuickFormat() + public void BuildPowerShellProvisioningScript_WhenFormattingFreshPartitions_FormatsByExplicitDriveLetter() { - IReadOnlyList script = WinPeUsbMediaService.BuildDiskPartScript( + string script = WinPeUsbMediaService.BuildPowerShellProvisioningScript( + diskNumber: 7, + partitionStyle: UsbPartitionStyle.Gpt, + formatMode: UsbFormatMode.Quick, + bootDriveLetter: 'S', + cacheDriveLetter: 'T'); + + Assert.DoesNotContain("diskpart", script, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("select partition", script, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("select volume", script, StringComparison.OrdinalIgnoreCase); + Assert.Contains("$bootDriveLetter = 'S'", script, StringComparison.Ordinal); + Assert.Contains("$cacheDriveLetter = 'T'", script, StringComparison.Ordinal); + Assert.Contains("DriveLetter = $bootDriveLetter", script, StringComparison.Ordinal); + Assert.Contains("DriveLetter = $cacheDriveLetter", script, StringComparison.Ordinal); + Assert.Contains("Format-Volume @bootFormatArguments", script, StringComparison.Ordinal); + Assert.Contains("Format-Volume @cacheFormatArguments", script, StringComparison.Ordinal); + } + + [Fact] + public void BuildPowerShellProvisioningScript_WhenFormattingUsb_EmitsProgressMarkers() + { + string script = WinPeUsbMediaService.BuildPowerShellProvisioningScript( + diskNumber: 7, + partitionStyle: UsbPartitionStyle.Gpt, + formatMode: UsbFormatMode.Quick, + bootDriveLetter: 'S', + cacheDriveLetter: 'T'); + + Assert.Contains("FOUNDRY_USB_PROGRESS|", script, StringComparison.Ordinal); + Assert.Contains("Write-FoundryUsbProgress 26 'Clearing USB partition table.'", script, StringComparison.Ordinal); + Assert.Contains("Write-FoundryUsbProgress 44 'Formatting BOOT partition.'", script, StringComparison.Ordinal); + Assert.Contains("Write-FoundryUsbProgress 53 'Formatting cache partition.'", script, StringComparison.Ordinal); + } + + [Fact] + public void BuildPowerShellProvisioningScript_WhenFormattingUsb_EmitsVerboseMarkers() + { + string script = WinPeUsbMediaService.BuildPowerShellProvisioningScript( + diskNumber: 7, + partitionStyle: UsbPartitionStyle.Gpt, + formatMode: UsbFormatMode.Quick, + bootDriveLetter: 'S', + cacheDriveLetter: 'T'); + + Assert.Contains("FOUNDRY_USB_VERBOSE|", script, StringComparison.Ordinal); + Assert.DoesNotContain("Write-Verbose", script, StringComparison.Ordinal); + Assert.Contains("Write-FoundryUsbVerbose \"Disk opened.", script, StringComparison.Ordinal); + Assert.Contains("Write-FoundryUsbVerbose \"BOOT partition created.", script, StringComparison.Ordinal); + Assert.Contains("Write-FoundryUsbVerbose \"Cache partition formatted.", script, StringComparison.Ordinal); + } + + [Fact] + public void BuildPowerShellProvisioningScript_WhenMbrCompleteFormat_MarksBootPartitionActiveAndFullFormat() + { + string script = WinPeUsbMediaService.BuildPowerShellProvisioningScript( diskNumber: 8, partitionStyle: UsbPartitionStyle.Mbr, formatMode: UsbFormatMode.Complete, bootDriveLetter: 'U', cacheDriveLetter: 'V'); - Assert.Contains("convert mbr", script); - Assert.DoesNotContain("convert mbr noerr", script); - Assert.DoesNotContain("convert gpt", script); - Assert.Contains("format fs=fat32 label=BOOT", script); - Assert.Contains("active", script); - Assert.Contains("format fs=ntfs label=\"Foundry Cache\"", script); + Assert.Contains("$diskNumber = 8", script, StringComparison.Ordinal); + Assert.Contains("$partitionStyle = 'MBR'", script, StringComparison.Ordinal); + Assert.Contains("$fullFormat = $true", script, StringComparison.Ordinal); + Assert.Contains("Set-Partition -DiskNumber $diskNumber -PartitionNumber $bootPartition.PartitionNumber -IsActive $true", script, StringComparison.Ordinal); + Assert.Contains("if ($fullFormat) { $bootFormatArguments['Full'] = $true }", script, StringComparison.Ordinal); + Assert.Contains("if ($fullFormat) { $cacheFormatArguments['Full'] = $true }", script, StringComparison.Ordinal); } [Fact] @@ -314,7 +372,79 @@ public async Task ProvisionAndPopulateAsync_WhenTargetIdentityIsUnsafe_ReturnsUn Assert.Contains("EncodedCommand", runner.Executions[0].Arguments, StringComparison.Ordinal); } - private sealed class FakeRunner(string output) : IWinPeProcessRunner + [Fact] + public async Task ProvisionAndPopulateAsync_WhenPartitioningUsb_UsesPowerShellStorageProvisioning() + { + string payload = """ + {"Number":9,"FriendlyName":"Safe USB","SerialNumber":"SERIAL","UniqueId":"UNIQUE","BusType":"USB","IsRemovable":true,"IsSystem":false,"IsBoot":false,"Size":64000000000} + """; + var runner = new FakeRunner(payload); + using TempWorkspace workspace = TempWorkspace.Create(); + var service = new WinPeUsbMediaService(runner); + + await service.ProvisionAndPopulateAsync( + new UsbOutputOptions + { + TargetDiskNumber = 9, + ExpectedDiskFriendlyName = "Safe USB", + PartitionStyle = UsbPartitionStyle.Gpt, + FormatMode = UsbFormatMode.Quick + }, + new WinPeBuildArtifact + { + WorkingDirectoryPath = workspace.RootPath, + MediaDirectoryPath = Path.Combine(workspace.RootPath, "media"), + Architecture = WinPeArchitecture.X64 + }, + new WinPeToolPaths { PowerShellPath = "pwsh.exe" }, + useBootEx: false, + CancellationToken.None); + + Assert.DoesNotContain("diskpart.exe", runner.Executions.Select(execution => execution.FileName)); + Assert.Equal(2, runner.Executions.Count(execution => execution.FileName == "pwsh.exe")); + } + + [Fact] + public async Task ProvisionAndPopulateAsync_WhenProvisioningStreamsOutput_ReportsProvisioningSubstepsAndVerboseDetails() + { + string payload = """ + {"Number":9,"FriendlyName":"Safe USB","SerialNumber":"SERIAL","UniqueId":"UNIQUE","BusType":"USB","IsRemovable":true,"IsSystem":false,"IsBoot":false,"Size":64000000000} + """; + var runner = new FakeOutputRunner(payload); + var progress = new RecordingProgress(); + using TempWorkspace workspace = TempWorkspace.Create(); + var service = new WinPeUsbMediaService(runner); + + await service.ProvisionAndPopulateAsync( + new UsbOutputOptions + { + TargetDiskNumber = 9, + ExpectedDiskFriendlyName = "Safe USB", + PartitionStyle = UsbPartitionStyle.Gpt, + FormatMode = UsbFormatMode.Quick, + Progress = progress + }, + new WinPeBuildArtifact + { + WorkingDirectoryPath = workspace.RootPath, + MediaDirectoryPath = Path.Combine(workspace.RootPath, "media"), + Architecture = WinPeArchitecture.X64 + }, + new WinPeToolPaths { PowerShellPath = "pwsh.exe" }, + useBootEx: false, + CancellationToken.None); + + Assert.Contains(progress.Reports, report => report is { Percent: 26, Status: "Clearing USB partition table." }); + Assert.Contains(progress.Reports, report => report is { Percent: 44, Status: "Formatting BOOT partition." }); + Assert.Contains(progress.Reports, report => report is { Percent: 53, Status: "Formatting cache partition." }); + Assert.Contains( + progress.Reports, + report => report.Percent == 44 && + report.Status == "Formatting BOOT partition." && + report.LogDetail == "BOOT partition formatted. DriveLetter=S, FileSystem=FAT32, Label=BOOT."); + } + + private class FakeRunner(string output) : IWinPeProcessRunner { public List Executions { get; } = []; @@ -356,6 +486,36 @@ public Task RunCmdScriptDirectAsync( } } + private sealed class FakeOutputRunner(string output) : FakeRunner(output), IWinPeProcessOutputRunner + { + public Task RunWithOutputAsync( + string fileName, + string arguments, + string workingDirectory, + Action? onOutputData, + Action? onErrorData, + CancellationToken cancellationToken, + IReadOnlyDictionary? environmentOverrides = null) + { + onOutputData?.Invoke("FOUNDRY_USB_PROGRESS|26|Clearing USB partition table."); + onOutputData?.Invoke("FOUNDRY_USB_PROGRESS|44|Formatting BOOT partition."); + onOutputData?.Invoke("FOUNDRY_USB_VERBOSE|BOOT partition formatted. DriveLetter=S, FileSystem=FAT32, Label=BOOT."); + onOutputData?.Invoke("FOUNDRY_USB_PROGRESS|53|Formatting cache partition."); + + return RunAsync(fileName, arguments, workingDirectory, cancellationToken, environmentOverrides); + } + } + + private sealed class RecordingProgress : IProgress + { + public List Reports { get; } = []; + + public void Report(WinPeMediaProgress value) + { + Reports.Add(value); + } + } + private sealed class TempWorkspace : IDisposable { private TempWorkspace(string rootPath) diff --git a/src/Foundry.Core/Services/WinPe/WinPeMediaProgress.cs b/src/Foundry.Core/Services/WinPe/WinPeMediaProgress.cs index e7a56fe9..1ff66955 100644 --- a/src/Foundry.Core/Services/WinPe/WinPeMediaProgress.cs +++ b/src/Foundry.Core/Services/WinPe/WinPeMediaProgress.cs @@ -4,4 +4,5 @@ public sealed record WinPeMediaProgress { public int Percent { get; init; } public string Status { get; init; } = string.Empty; + public string LogDetail { get; init; } = string.Empty; } diff --git a/src/Foundry.Core/Services/WinPe/WinPeUsbMediaService.cs b/src/Foundry.Core/Services/WinPe/WinPeUsbMediaService.cs index 343e5fdb..08682f0b 100644 --- a/src/Foundry.Core/Services/WinPe/WinPeUsbMediaService.cs +++ b/src/Foundry.Core/Services/WinPe/WinPeUsbMediaService.cs @@ -5,6 +5,9 @@ namespace Foundry.Core.Services.WinPe; public sealed class WinPeUsbMediaService : IWinPeUsbMediaService { + private const string UsbProvisioningProgressPrefix = "FOUNDRY_USB_PROGRESS|"; + private const string UsbProvisioningVerbosePrefix = "FOUNDRY_USB_VERBOSE|"; + private readonly IWinPeProcessRunner _processRunner; private readonly IWinPeRuntimePayloadProvisioningService _runtimePayloadProvisioningService; @@ -126,6 +129,7 @@ public async Task> ProvisionAndPopulateAsyn { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(artifact); + ArgumentNullException.ThrowIfNull(tools); cancellationToken.ThrowIfCancellationRequested(); @@ -181,7 +185,9 @@ public async Task> ProvisionAndPopulateAsyn options.FormatMode, bootDriveLetter, cacheDriveLetter, + tools, artifact.WorkingDirectoryPath, + options.Progress, cancellationToken).ConfigureAwait(false); if (!provisioningResult.IsSuccess) { @@ -320,42 +326,102 @@ internal static bool IsRobocopySuccessExitCode(int exitCode) return exitCode is >= 0 and <= 7; } - internal static IReadOnlyList BuildDiskPartScript( + internal static string BuildPowerShellProvisioningScript( int diskNumber, UsbPartitionStyle partitionStyle, UsbFormatMode formatMode, char bootDriveLetter, char cacheDriveLetter) { - string[] conversionLines = partitionStyle == UsbPartitionStyle.Gpt - ? ["convert mbr noerr", "convert gpt"] - : ["convert mbr"]; - string activeLine = partitionStyle == UsbPartitionStyle.Mbr ? "active" : string.Empty; - string formatSuffix = formatMode == UsbFormatMode.Quick ? " quick" : string.Empty; + string partitionStyleText = partitionStyle == UsbPartitionStyle.Gpt ? "GPT" : "MBR"; + string fullFormatValue = formatMode == UsbFormatMode.Complete ? "$true" : "$false"; + char normalizedBootDriveLetter = char.ToUpperInvariant(bootDriveLetter); + char normalizedCacheDriveLetter = char.ToUpperInvariant(cacheDriveLetter); + string activeLine = partitionStyle == UsbPartitionStyle.Mbr + ? "Set-Partition -DiskNumber $diskNumber -PartitionNumber $bootPartition.PartitionNumber -IsActive $true -ErrorAction Stop" + : string.Empty; string[] scriptLines = [ - $"select disk {diskNumber}", - "online disk noerr", - "attributes disk clear readonly noerr", - "clean", - ..conversionLines, - "create partition primary size=4096", - "select partition 1", - $"assign letter={bootDriveLetter}", - $"select volume={bootDriveLetter}", - $"format fs=fat32{formatSuffix} label=BOOT", + "$ErrorActionPreference = 'Stop'", + "Import-Module Storage", + "function Write-FoundryUsbProgress([int]$Percent, [string]$Status) {", + " Write-Output (\"FOUNDRY_USB_PROGRESS|{0}|{1}\" -f $Percent, $Status)", + "}", + "function Write-FoundryUsbVerbose([string]$Message) {", + " Write-Output (\"FOUNDRY_USB_VERBOSE|{0}\" -f $Message)", + "}", + $"$diskNumber = {diskNumber}", + $"$partitionStyle = '{partitionStyleText}'", + $"$fullFormat = {fullFormatValue}", + $"$bootDriveLetter = '{normalizedBootDriveLetter}'", + $"$cacheDriveLetter = '{normalizedCacheDriveLetter}'", + "Write-FoundryUsbVerbose \"Provisioning disk $diskNumber. PartitionStyle=$partitionStyle, FullFormat=$fullFormat, BootDriveLetter=$bootDriveLetter, CacheDriveLetter=$cacheDriveLetter.\"", + "Write-FoundryUsbProgress 21 'Opening USB disk.'", + "$disk = Get-Disk -Number $diskNumber -ErrorAction Stop", + "Write-FoundryUsbVerbose \"Disk opened. Number=$($disk.Number), FriendlyName=$($disk.FriendlyName), PartitionStyle=$($disk.PartitionStyle), Size=$($disk.Size), IsOffline=$($disk.IsOffline), IsReadOnly=$($disk.IsReadOnly).\"", + "Write-FoundryUsbProgress 23 'Preparing USB disk attributes.'", + "if ($disk.IsOffline) { Set-Disk -Number $diskNumber -IsOffline $false -ErrorAction Stop }", + "if ($disk.IsReadOnly) { Set-Disk -Number $diskNumber -IsReadOnly $false -ErrorAction Stop }", + "Write-FoundryUsbVerbose 'USB disk attributes prepared.'", + "Write-FoundryUsbProgress 26 'Clearing USB partition table.'", + "Clear-Disk -Number $diskNumber -RemoveData -RemoveOEM -Confirm:$false -ErrorAction Stop", + "Update-HostStorageCache -ErrorAction SilentlyContinue", + "Write-FoundryUsbVerbose 'USB partition table cleared and host storage cache refreshed.'", + "Write-FoundryUsbProgress 32 'Initializing USB partition table.'", + "$disk = Get-Disk -Number $diskNumber -ErrorAction Stop", + "if ($disk.PartitionStyle -eq 'RAW') {", + " Initialize-Disk -Number $diskNumber -PartitionStyle $partitionStyle -ErrorAction Stop", + "} elseif ([string]$disk.PartitionStyle -ne $partitionStyle) {", + " throw \"Disk $diskNumber could not be reset to $partitionStyle. Current partition style: $($disk.PartitionStyle).\"", + "}", + "$disk = Get-Disk -Number $diskNumber -ErrorAction Stop", + "Write-FoundryUsbVerbose \"USB partition table initialized. CurrentPartitionStyle=$($disk.PartitionStyle).\"", + "Write-FoundryUsbProgress 38 'Creating BOOT partition.'", + "$bootPartition = New-Partition -DiskNumber $diskNumber -Size 4096MB -DriveLetter $bootDriveLetter -ErrorAction Stop", + "Write-FoundryUsbVerbose \"BOOT partition created. PartitionNumber=$($bootPartition.PartitionNumber), DriveLetter=$($bootPartition.DriveLetter), Size=$($bootPartition.Size).\"", activeLine, - "create partition primary", - "select partition 2", - $"assign letter={cacheDriveLetter}", - $"select volume={cacheDriveLetter}", - $"format fs=ntfs{formatSuffix} label=\"Foundry Cache\"" + "if ($partitionStyle -eq 'MBR') { Write-FoundryUsbVerbose \"BOOT partition marked active. PartitionNumber=$($bootPartition.PartitionNumber).\" }", + "Write-FoundryUsbProgress 44 'Formatting BOOT partition.'", + "$bootFormatArguments = @{", + " DriveLetter = $bootDriveLetter", + " FileSystem = 'FAT32'", + " NewFileSystemLabel = 'BOOT'", + " Confirm = $false", + " Force = $true", + " ErrorAction = 'Stop'", + "}", + "if ($fullFormat) { $bootFormatArguments['Full'] = $true }", + "Format-Volume @bootFormatArguments | Out-Null", + "Write-FoundryUsbVerbose \"BOOT partition formatted. DriveLetter=$bootDriveLetter, FileSystem=FAT32, Label=BOOT.\"", + "Write-FoundryUsbProgress 49 'Creating cache partition.'", + "$cachePartition = New-Partition -DiskNumber $diskNumber -UseMaximumSize -DriveLetter $cacheDriveLetter -ErrorAction Stop", + "Write-FoundryUsbVerbose \"Cache partition created. PartitionNumber=$($cachePartition.PartitionNumber), DriveLetter=$($cachePartition.DriveLetter), Size=$($cachePartition.Size).\"", + "Write-FoundryUsbProgress 53 'Formatting cache partition.'", + "$cacheFormatArguments = @{", + " DriveLetter = $cacheDriveLetter", + " FileSystem = 'NTFS'", + " NewFileSystemLabel = 'Foundry Cache'", + " Confirm = $false", + " Force = $true", + " ErrorAction = 'Stop'", + "}", + "if ($fullFormat) { $cacheFormatArguments['Full'] = $true }", + "Format-Volume @cacheFormatArguments | Out-Null", + "Write-FoundryUsbVerbose \"Cache partition formatted. DriveLetter=$cacheDriveLetter, FileSystem=NTFS, Label=Foundry Cache.\"", + "Write-FoundryUsbProgress 55 'USB partitions formatted.'", + "[pscustomobject]@{", + " DiskNumber = $diskNumber", + " BootDriveLetter = \"$bootDriveLetter`:\"", + " CacheDriveLetter = \"$cacheDriveLetter`:\"", + "} | ConvertTo-Json -Compress" ]; - return scriptLines + return string.Join( + Environment.NewLine, + scriptLines .Where(line => !string.IsNullOrWhiteSpace(line)) - .ToArray(); + .ToArray()); } internal static void InitializeCachePartitionDirectories(string cacheRootPath) @@ -470,25 +536,35 @@ private async Task ProvisionDiskAsync( UsbFormatMode formatMode, char bootDriveLetter, char cacheDriveLetter, + WinPeToolPaths tools, string workingDirectoryPath, + IProgress? progress, CancellationToken cancellationToken) { - IReadOnlyList diskPartScript = BuildDiskPartScript( + string script = BuildPowerShellProvisioningScript( diskNumber, partitionStyle, formatMode, bootDriveLetter, cacheDriveLetter); - string scriptPath = Path.Combine(workingDirectoryPath, "diskpart-usb.txt"); Directory.CreateDirectory(workingDirectoryPath); - await File.WriteAllLinesAsync(scriptPath, diskPartScript, cancellationToken).ConfigureAwait(false); - - WinPeProcessExecution execution = await _processRunner.RunAsync( - "diskpart.exe", - $"/s {WinPeProcessRunner.Quote(scriptPath)}", - workingDirectoryPath, - cancellationToken).ConfigureAwait(false); + string encodedScript = Convert.ToBase64String(Encoding.Unicode.GetBytes(script)); + string arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {encodedScript}"; + var provisioningOutput = new UsbProvisioningOutputForwarder(progress); + WinPeProcessExecution execution = _processRunner is IWinPeProcessOutputRunner outputRunner + ? await outputRunner.RunWithOutputAsync( + tools.PowerShellPath, + arguments, + workingDirectoryPath, + provisioningOutput.Report, + null, + cancellationToken).ConfigureAwait(false) + : await _processRunner.RunAsync( + tools.PowerShellPath, + arguments, + workingDirectoryPath, + cancellationToken).ConfigureAwait(false); if (execution.IsSuccess) { @@ -497,8 +573,8 @@ private async Task ProvisionDiskAsync( string diagnostic = $"{execution.ToDiagnosticText()}{Environment.NewLine}" + $"PartitionStyle: {partitionStyle}{Environment.NewLine}" + - "DiskPartScript:" + Environment.NewLine + - string.Join(Environment.NewLine, diskPartScript); + "PowerShellProvisioningScript:" + Environment.NewLine + + script; return WinPeResult.Failure( WinPeErrorCodes.UsbProvisioningFailed, "Failed to partition and format the USB disk.", @@ -546,6 +622,46 @@ private static void ReportProgress(IProgress? progress, int }); } + private sealed class UsbProvisioningOutputForwarder(IProgress? progress) + { + private int currentPercent = 20; + private string currentStatus = "Partitioning and formatting USB target."; + + public void Report(string line) + { + if (progress is null) + { + return; + } + + if (line.StartsWith(UsbProvisioningProgressPrefix, StringComparison.Ordinal)) + { + string payload = line[UsbProvisioningProgressPrefix.Length..]; + string[] parts = payload.Split('|', 2); + if (parts.Length != 2 || !int.TryParse(parts[0], out int percent)) + { + return; + } + + currentPercent = percent; + currentStatus = parts[1]; + ReportProgress(progress, currentPercent, currentStatus); + return; + } + + string verboseLine = line.StartsWith(UsbProvisioningVerbosePrefix, StringComparison.Ordinal) + ? line[UsbProvisioningVerbosePrefix.Length..] + : line; + + progress.Report(new WinPeMediaProgress + { + Percent = currentPercent, + Status = currentStatus, + LogDetail = verboseLine + }); + } + } + private async Task> GetDiskIdentityAsync( int diskNumber, WinPeToolPaths tools, diff --git a/src/Foundry/Strings/en-US/Resources.resw b/src/Foundry/Strings/en-US/Resources.resw index 1c9e0080..dfb37462 100644 --- a/src/Foundry/Strings/en-US/Resources.resw +++ b/src/Foundry/Strings/en-US/Resources.resw @@ -726,6 +726,33 @@ Partitioning and formatting USB target. + + Opening USB disk. + + + Preparing USB disk attributes. + + + Clearing USB partition table. + + + Initializing USB partition table. + + + Creating BOOT partition. + + + Formatting BOOT partition. + + + Creating cache partition. + + + Formatting cache partition. + + + USB partitions formatted. + Copying WinPE media to USB. diff --git a/src/Foundry/Strings/fr-FR/Resources.resw b/src/Foundry/Strings/fr-FR/Resources.resw index 3a5b147d..7abc4643 100644 --- a/src/Foundry/Strings/fr-FR/Resources.resw +++ b/src/Foundry/Strings/fr-FR/Resources.resw @@ -726,6 +726,33 @@ Partitionnement et formatage de la cible USB. + + Ouverture du disque USB. + + + Préparation des attributs du disque USB. + + + Effacement de la table de partitions USB. + + + Initialisation de la table de partitions USB. + + + Création de la partition BOOT. + + + Formatage de la partition BOOT. + + + Création de la partition cache. + + + Formatage de la partition cache. + + + Partitions USB formatées. + Copie du média WinPE vers l'USB. diff --git a/src/Foundry/ViewModels/StartMediaViewModel.cs b/src/Foundry/ViewModels/StartMediaViewModel.cs index 43a75c50..5573727f 100644 --- a/src/Foundry/ViewModels/StartMediaViewModel.cs +++ b/src/Foundry/ViewModels/StartMediaViewModel.cs @@ -779,10 +779,11 @@ private void ReportFinalMediaProgress(WinPeMediaProgress progress) : LocalizeFinalMediaStatus(progress.Status); logger.Debug( - "Final media output progress changed. CoreStatus={CoreStatus}, Percent={Percent}, NormalizedProgress={NormalizedProgress}", + "Final media output progress changed. CoreStatus={CoreStatus}, Percent={Percent}, NormalizedProgress={NormalizedProgress}, LogDetail={LogDetail}", progress.Status, progress.Percent, - normalizedProgress); + normalizedProgress, + progress.LogDetail); operationProgressService.Report(normalizedProgress, status); } @@ -890,6 +891,15 @@ private string LocalizeFinalMediaStatus(string status) "Validating USB target." => "StartMedia.Operation.ValidatingUsbTarget", "Checking USB target safety." => "StartMedia.Operation.CheckingUsbTargetSafety", "Partitioning and formatting USB target." => "StartMedia.Operation.PartitioningUsbTarget", + "Opening USB disk." => "StartMedia.Operation.OpeningUsbDisk", + "Preparing USB disk attributes." => "StartMedia.Operation.PreparingUsbDiskAttributes", + "Clearing USB partition table." => "StartMedia.Operation.ClearingUsbPartitionTable", + "Initializing USB partition table." => "StartMedia.Operation.InitializingUsbPartitionTable", + "Creating BOOT partition." => "StartMedia.Operation.CreatingUsbBootPartition", + "Formatting BOOT partition." => "StartMedia.Operation.FormattingUsbBootPartition", + "Creating cache partition." => "StartMedia.Operation.CreatingUsbCachePartition", + "Formatting cache partition." => "StartMedia.Operation.FormattingUsbCachePartition", + "USB partitions formatted." => "StartMedia.Operation.UsbPartitionsFormatted", "Copying WinPE media to USB." => "StartMedia.Operation.CopyingUsbMedia", "Configuring USB boot files." => "StartMedia.Operation.ConfiguringUsbBootFiles", "Verifying USB boot media." => "StartMedia.Operation.VerifyingUsbMedia",