diff --git a/src/Installer/Microsoft.Dotnet.Installation/IProgressTarget.cs b/src/Installer/Microsoft.Dotnet.Installation/IProgressTarget.cs new file mode 100644 index 000000000000..eb8779adc702 --- /dev/null +++ b/src/Installer/Microsoft.Dotnet.Installation/IProgressTarget.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Dotnet.Installation; + +public interface IProgressTarget +{ + public IProgressReporter CreateProgressReporter(); +} + +public interface IProgressReporter : IDisposable +{ + public IProgressTask AddTask(string description, double maxValue); +} + +public interface IProgressTask +{ + string Description { get; set; } + double Value { get; set; } + double MaxValue { get; set; } + + +} + +public class NullProgressTarget : IProgressTarget +{ + public IProgressReporter CreateProgressReporter() => new NullProgressReporter(); + class NullProgressReporter : IProgressReporter + { + public void Dispose() + { + } + public IProgressTask AddTask(string description, double maxValue) + { + return new NullProgressTask(description); + } + } + class NullProgressTask : IProgressTask + { + public NullProgressTask(string description) + { + Description = description; + } + + public double Value { get; set; } + public string Description { get; set; } + public double MaxValue { get; set; } + } +} diff --git a/src/Installer/Microsoft.Dotnet.Installation/InstallerFactory.cs b/src/Installer/Microsoft.Dotnet.Installation/InstallerFactory.cs index a27a5c2ecdaf..990f830be913 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/InstallerFactory.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/InstallerFactory.cs @@ -10,9 +10,9 @@ namespace Microsoft.Dotnet.Installation; public static class InstallerFactory { - public static IDotnetInstaller CreateInstaller() + public static IDotnetInstaller CreateInstaller(IProgressTarget progressTarget) { - return new DotnetInstaller(); + return new DotnetInstaller(progressTarget); } public static IDotnetReleaseInfoProvider CreateReleaseInfoProvider() diff --git a/src/Installer/Microsoft.Dotnet.Installation/Internal/ArchiveDotnetExtractor.cs b/src/Installer/Microsoft.Dotnet.Installation/Internal/ArchiveDotnetExtractor.cs index 83b29ea5d807..acd883107d69 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Internal/ArchiveDotnetExtractor.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/Internal/ArchiveDotnetExtractor.cs @@ -16,15 +16,15 @@ internal class ArchiveDotnetExtractor : IDisposable { private readonly DotnetInstallRequest _request; private readonly ReleaseVersion _resolvedVersion; - private readonly bool _noProgress; + private readonly IProgressTarget _progressTarget; private string scratchDownloadDirectory; private string? _archivePath; - public ArchiveDotnetExtractor(DotnetInstallRequest request, ReleaseVersion resolvedVersion, bool noProgress = false) + public ArchiveDotnetExtractor(DotnetInstallRequest request, ReleaseVersion resolvedVersion, IProgressTarget progressTarget) { _request = request; _resolvedVersion = resolvedVersion; - _noProgress = noProgress; + _progressTarget = progressTarget; scratchDownloadDirectory = Directory.CreateTempSubdirectory().FullName; } @@ -34,33 +34,17 @@ public void Prepare() var archiveName = $"dotnet-{Guid.NewGuid()}"; _archivePath = Path.Combine(scratchDownloadDirectory, archiveName + DnupUtilities.GetArchiveFileExtensionForPlatform()); - if (_noProgress) + using (var progressReporter = _progressTarget.CreateProgressReporter()) { - // When no-progress is enabled, download without progress display - Console.WriteLine($"Downloading .NET SDK {_resolvedVersion}..."); - var downloadSuccess = releaseManifest.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, null); + var downloadTask = progressReporter.AddTask($"Downloading .NET SDK {_resolvedVersion}", 100); + var reporter = new DownloadProgressReporter(downloadTask, $"Downloading .NET SDK {_resolvedVersion}"); + var downloadSuccess = releaseManifest.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, reporter); if (!downloadSuccess) { throw new InvalidOperationException($"Failed to download .NET archive for version {_resolvedVersion}"); } - Console.WriteLine($"Download of .NET SDK {_resolvedVersion} complete."); - } - else - { - // Use progress display for normal operation - Spectre.Console.AnsiConsole.Progress() - .Start(ctx => - { - var downloadTask = ctx.AddTask($"Downloading .NET SDK {_resolvedVersion}", autoStart: true); - var reporter = new SpectreDownloadProgressReporter(downloadTask, $"Downloading .NET SDK {_resolvedVersion}"); - var downloadSuccess = releaseManifest.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, reporter); - if (!downloadSuccess) - { - throw new InvalidOperationException($"Failed to download .NET archive for version {_resolvedVersion}"); - } - downloadTask.Value = 100; - }); + downloadTask.Value = 100; } } @@ -111,37 +95,18 @@ public void Commit(IEnumerable existingSdkVersions) throw new InvalidOperationException("Archive not found. Make sure Prepare() was called successfully."); } - if (_noProgress) + using (var progressReporter = _progressTarget.CreateProgressReporter()) { - // When no-progress is enabled, install without progress display - Console.WriteLine($"Installing .NET SDK {_resolvedVersion}..."); + var installTask = progressReporter.AddTask($"Installing .NET SDK {_resolvedVersion}", maxValue: 100); // Extract archive directly to target directory with special handling for muxer - var extractResult = ExtractArchiveDirectlyToTarget(_archivePath, _request.InstallRoot.Path!, existingSdkVersions, null); + var extractResult = ExtractArchiveDirectlyToTarget(_archivePath, _request.InstallRoot.Path!, existingSdkVersions, installTask); if (extractResult is not null) { throw new InvalidOperationException($"Failed to install SDK: {extractResult}"); } - Console.WriteLine($"Installation of .NET SDK {_resolvedVersion} complete."); - } - else - { - // Use progress display for normal operation - Spectre.Console.AnsiConsole.Progress() - .Start(ctx => - { - var installTask = ctx.AddTask($"Installing .NET SDK {_resolvedVersion}", autoStart: true); - - // Extract archive directly to target directory with special handling for muxer - var extractResult = ExtractArchiveDirectlyToTarget(_archivePath, _request.InstallRoot.Path!, existingSdkVersions, installTask); - if (extractResult is not null) - { - throw new InvalidOperationException($"Failed to install SDK: {extractResult}"); - } - - installTask.Value = installTask.MaxValue; - }); + installTask.Value = installTask.MaxValue; } } @@ -149,7 +114,7 @@ public void Commit(IEnumerable existingSdkVersions) * Extracts the archive directly to the target directory with special handling for muxer. * Combines extraction and installation into a single operation. */ - private string? ExtractArchiveDirectlyToTarget(string archivePath, string targetDir, IEnumerable existingSdkVersions, Spectre.Console.ProgressTask? installTask) + private string? ExtractArchiveDirectlyToTarget(string archivePath, string targetDir, IEnumerable existingSdkVersions, IProgressTask? installTask) { try { @@ -194,7 +159,7 @@ private MuxerHandlingConfig ConfigureMuxerHandling(IEnumerable e /** * Extracts a tar or tar.gz archive to the target directory. */ - private string? ExtractTarArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, Spectre.Console.ProgressTask? installTask) + private string? ExtractTarArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { string decompressedPath = DecompressTarGzIfNeeded(archivePath, out bool needsDecompression); @@ -265,7 +230,7 @@ private long CountTarEntries(string tarPath) /** * Extracts the contents of a tar file to the target directory. */ - private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingConfig muxerConfig, Spectre.Console.ProgressTask? installTask) + private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { using var tarStream = File.OpenRead(tarPath); var tarReader = new TarReader(tarStream); @@ -282,12 +247,12 @@ private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingC // Create directory if it doesn't exist var dirPath = Path.Combine(targetDir, entry.Name); Directory.CreateDirectory(dirPath); - installTask?.Increment(1); + installTask?.Value += 1; } else { // Skip other entry types - installTask?.Increment(1); + installTask?.Value += 1; } } } @@ -295,7 +260,7 @@ private void ExtractTarContents(string tarPath, string targetDir, MuxerHandlingC /** * Extracts a single file entry from a tar archive. */ - private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, Spectre.Console.ProgressTask? installTask) + private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { var fileName = Path.GetFileName(entry.Name); var destPath = Path.Combine(targetDir, entry.Name); @@ -314,7 +279,7 @@ private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandling entry.DataStream?.CopyTo(outStream); } - installTask?.Increment(1); + installTask?.Value += 1; } /** @@ -346,7 +311,7 @@ private void HandleMuxerUpdateFromTar(TarEntry entry, string muxerTargetPath) /** * Extracts a zip archive to the target directory. */ - private string? ExtractZipArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, Spectre.Console.ProgressTask? installTask) + private string? ExtractZipArchive(string archivePath, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { long totalFiles = CountZipEntries(archivePath); @@ -373,7 +338,7 @@ private long CountZipEntries(string zipPath) /** * Extracts a single entry from a zip archive. */ - private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, Spectre.Console.ProgressTask? installTask) + private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandlingConfig muxerConfig, IProgressTask? installTask) { var fileName = Path.GetFileName(entry.FullName); var destPath = Path.Combine(targetDir, entry.FullName); @@ -382,7 +347,7 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl if (string.IsNullOrEmpty(fileName)) { Directory.CreateDirectory(destPath); - installTask?.Increment(1); + installTask?.Value += 1; return; } @@ -400,7 +365,7 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl entry.ExtractToFile(destPath, overwrite: true); } - installTask?.Increment(1); + installTask?.Value += 1; } /** diff --git a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetInstaller.cs b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetInstaller.cs index 3d08a84050d6..40fd2a48025f 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetInstaller.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetInstaller.cs @@ -5,17 +5,23 @@ using System.Collections.Generic; using System.Text; using Microsoft.Deployment.DotNet.Releases; -using Spectre.Console; namespace Microsoft.Dotnet.Installation.Internal { internal class DotnetInstaller : IDotnetInstaller { + IProgressTarget _progressTarget; + + public DotnetInstaller(IProgressTarget progressTarget) + { + _progressTarget = progressTarget; + } + public void Install(DotnetInstallRoot dotnetRoot, InstallComponent component, ReleaseVersion version) { var installRequest = new DotnetInstallRequest(dotnetRoot, new UpdateChannel(version.ToString()), component, new InstallRequestOptions()); - using ArchiveDotnetExtractor installer = new(installRequest, version, noProgress: true); + using ArchiveDotnetExtractor installer = new(installRequest, version, _progressTarget); installer.Prepare(); installer.Commit(); } diff --git a/src/Installer/Microsoft.Dotnet.Installation/Internal/SpectreDownloadProgressReporter.cs b/src/Installer/Microsoft.Dotnet.Installation/Internal/DownloadProgressReporter.cs similarity index 84% rename from src/Installer/Microsoft.Dotnet.Installation/Internal/SpectreDownloadProgressReporter.cs rename to src/Installer/Microsoft.Dotnet.Installation/Internal/DownloadProgressReporter.cs index 9a0fa920ad5d..625742b2e6da 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Internal/SpectreDownloadProgressReporter.cs +++ b/src/Installer/Microsoft.Dotnet.Installation/Internal/DownloadProgressReporter.cs @@ -1,16 +1,15 @@ using System; -using Spectre.Console; using Microsoft.Dotnet.Installation; namespace Microsoft.Dotnet.Installation.Internal { - public class SpectreDownloadProgressReporter : IProgress + public class DownloadProgressReporter : IProgress { - private readonly ProgressTask _task; + private readonly IProgressTask _task; private readonly string _description; private long? _totalBytes; - public SpectreDownloadProgressReporter(ProgressTask task, string description) + public DownloadProgressReporter(IProgressTask task, string description) { _task = task; _description = description; diff --git a/src/Installer/Microsoft.Dotnet.Installation/Microsoft.Dotnet.Installation.csproj b/src/Installer/Microsoft.Dotnet.Installation/Microsoft.Dotnet.Installation.csproj index ec1119864671..42197569ad78 100644 --- a/src/Installer/Microsoft.Dotnet.Installation/Microsoft.Dotnet.Installation.csproj +++ b/src/Installer/Microsoft.Dotnet.Installation/Microsoft.Dotnet.Installation.csproj @@ -19,8 +19,6 @@ - - diff --git a/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs b/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs index e1cb04ec401d..89957ad9106d 100644 --- a/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs +++ b/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs @@ -209,7 +209,7 @@ public override int Execute() // TODO: Implement transaction / rollback? - SpectreAnsiConsole.MarkupInterpolated($"Installing .NET SDK [blue]{resolvedVersion}[/] to [blue]{resolvedInstallPath}[/]..."); + SpectreAnsiConsole.MarkupLineInterpolated($"Installing .NET SDK [blue]{resolvedVersion}[/] to [blue]{resolvedInstallPath}[/]..."); DotnetInstall? mainInstall; diff --git a/src/Installer/dnup/InstallerOrchestratorSingleton.cs b/src/Installer/dnup/InstallerOrchestratorSingleton.cs index a1610c21c0c5..bc2c990a7f6d 100644 --- a/src/Installer/dnup/InstallerOrchestratorSingleton.cs +++ b/src/Installer/dnup/InstallerOrchestratorSingleton.cs @@ -51,7 +51,9 @@ private InstallerOrchestratorSingleton() } } - using ArchiveDotnetExtractor installer = new(installRequest, versionToInstall, noProgress); + IProgressTarget progressTarget = noProgress ? new NonUpdatingProgressTarget() : new SpectreProgressTarget(); + + using ArchiveDotnetExtractor installer = new(installRequest, versionToInstall, progressTarget); installer.Prepare(); // Extract and commit the install to the directory diff --git a/src/Installer/dnup/NonUpdatingProgressTarget.cs b/src/Installer/dnup/NonUpdatingProgressTarget.cs new file mode 100644 index 000000000000..5903cd319f7f --- /dev/null +++ b/src/Installer/dnup/NonUpdatingProgressTarget.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata.Ecma335; +using System.Text; + +namespace Microsoft.DotNet.Tools.Bootstrapper; + +public class NonUpdatingProgressTarget : IProgressTarget +{ + public IProgressReporter CreateProgressReporter() => new Reporter(); + + class Reporter : IProgressReporter + { + List _tasks = new(); + + public IProgressTask AddTask(string description, double maxValue) + { + var task = new ProgressTaskImpl(description) + { + MaxValue = maxValue + }; + _tasks.Add(task); + Spectre.Console.AnsiConsole.WriteLine(description + "..."); + return task; + } + public void Dispose() + { + foreach (var task in _tasks) + { + task.Complete(); + } + } + } + + class ProgressTaskImpl : IProgressTask + { + bool _completed = false; + double _value; + + public ProgressTaskImpl(string description) + { + Description = description; + } + + public double Value + { + get => _value; + set + { + _value = value; + if (_value >= MaxValue) + { + Complete(); + } + } + } + public string Description { get; set; } + public double MaxValue { get; set; } + + public void Complete() + { + if (!_completed) + { + Spectre.Console.AnsiConsole.MarkupLine($"[green]Completed:[/] {Description}"); + _completed = true; + } + } + } +} diff --git a/src/Installer/dnup/SpectreProgressTarget.cs b/src/Installer/dnup/SpectreProgressTarget.cs new file mode 100644 index 000000000000..b1998bf09942 --- /dev/null +++ b/src/Installer/dnup/SpectreProgressTarget.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using Spectre.Console; + +namespace Microsoft.DotNet.Tools.Bootstrapper; + +public class SpectreProgressTarget : IProgressTarget +{ + public IProgressReporter CreateProgressReporter() => new Reporter(); + + class Reporter : IProgressReporter + { + TaskCompletionSource _overallTask = new TaskCompletionSource(); + ProgressContext _progressContext; + + public Reporter() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + var progressTask = AnsiConsole.Progress().StartAsync(async ctx => + { + tcs.SetResult(ctx); + await _overallTask.Task; + }); + + _progressContext = tcs.Task.GetAwaiter().GetResult(); + } + + public IProgressTask AddTask(string description, double maxValue) + { + return new ProgressTaskImpl(_progressContext.AddTask(description, maxValue: maxValue)); + } + + public void Dispose() + { + _overallTask.SetResult(); + } + } + + class ProgressTaskImpl : IProgressTask + { + private readonly Spectre.Console.ProgressTask _task; + public ProgressTaskImpl(Spectre.Console.ProgressTask task) + { + _task = task; + } + + public double Value + { + get => _task.Value; + set => _task.Value = value; + } + public string Description + { + get => _task.Description; + set => _task.Description = value; + } + public double MaxValue + { + get => _task.MaxValue; + set => _task.MaxValue = value; + } + } +} diff --git a/test/dnup.Tests/LibraryTests.cs b/test/dnup.Tests/LibraryTests.cs index 0b3c68fc4b0f..217116c0c1eb 100644 --- a/test/dnup.Tests/LibraryTests.cs +++ b/test/dnup.Tests/LibraryTests.cs @@ -29,7 +29,7 @@ public void LatestVersionForChannelCanBeInstalled(string channel) var latestVersion = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, channel); Log.WriteLine($"Latest version for channel '{channel}' is '{latestVersion}'"); - var installer = InstallerFactory.CreateInstaller(); + var installer = InstallerFactory.CreateInstaller(new NullProgressTarget()); using var testEnv = DnupTestUtilities.CreateTestEnvironment();