Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to download multiple VM images at the same time from the Hyper-V gallery #3687

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,52 @@
// Licensed under the MIT License.

namespace HyperVExtension.Models;

public enum TransferStatus
{
NotStarted,
InProgress,
Succeeded,
Failed,
}

/// <summary>
/// Represents progress of an operation that require transferring bytes from one place to another.
/// </summary>
public class ByteTransferProgress
{
public long BytesReceived { get; set; }
{
private readonly TransferStatus _transferStatus;

public long TotalBytesToReceive { get; set; }
public long BytesReceived { get; }

public long TotalBytesToReceive { get; }

public uint PercentageComplete => (uint)((BytesReceived / (double)TotalBytesToReceive) * 100);

public ByteTransferProgress(long bytesReceived, long totalBytesToReceive)
public string ErrorMessage { get; } = string.Empty;

public ByteTransferProgress()
{
_transferStatus = TransferStatus.NotStarted;
}

public ByteTransferProgress(
long bytesReceived,
long totalBytesToReceive,
TransferStatus transferStatus = TransferStatus.InProgress)
{
BytesReceived = bytesReceived;
TotalBytesToReceive = totalBytesToReceive;
TotalBytesToReceive = totalBytesToReceive;
_transferStatus = transferStatus;
}

public ByteTransferProgress(string errorMessage)
{
ErrorMessage = errorMessage;
_transferStatus = TransferStatus.Failed;
}

public bool Succeeded => _transferStatus == TransferStatus.Succeeded;

public bool Failed => _transferStatus == TransferStatus.Failed;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ namespace HyperVExtension.Models.VirtualMachineCreation;
/// <see cref="ZipArchive"/> is used by Hyper-V Manager's VM Quick Create feature. This gives us parity, but in the future it is expected that faster/more efficient
/// archive extraction libraries will be added.
/// </summary>
/// <remarks>
/// .Net core's ZipFile and ZipArchive implementations for extracting large files (GBs) are slow when used in Dev Homes Debug configuration.
/// In release they are much quicker. To experience downloads from the users point of view build with the release configuration.
/// </remarks>
public sealed class DotNetZipArchiveProvider : IArchiveProvider
{
// Same buffer size used by Hyper-V Manager's VM gallery feature.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using HyperVExtension.Extensions;
using Serilog;

namespace HyperVExtension.Models.VirtualMachineCreation;

/// <summary>
/// Monitors the download of a file and allows clients to subscribe to the progress of the
/// download. As new progress comes in the FileDownloadMonitor publishes this progress to
/// its subscribers.
/// </summary>
internal sealed class FileDownloadMonitor
{
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(FileDownloadMonitor));

private readonly object _lock = new();

private readonly List<IProgress<IOperationReport>> _subscriberList = new();

private readonly Progress<ByteTransferProgress> _progressReporter;

private bool _downloadInProgress;

private DownloadOperationReport _lastSentReport = new(new ByteTransferProgress());

public FileDownloadMonitor(IProgress<IOperationReport> progressSubscriber)
{
AddSubscriber(progressSubscriber);
_progressReporter = new(PublishProgress);
}

public void AddSubscriber(IProgress<IOperationReport> progressSubscriber)
{
lock (_lock)
{
if (_lastSentReport != null)
{
// Subscriber addition requested so we'll submit the last recorded
// progress we received before adding it to the subscriber list
progressSubscriber.Report(_lastSentReport);
}

_subscriberList.Add(progressSubscriber);
}
}

private void PublishProgress(ByteTransferProgress transferProgress)
{
lock (_lock)
{
_lastSentReport = new DownloadOperationReport(transferProgress);
_subscriberList.ForEach(subscriber => subscriber.Report(_lastSentReport));
}
}

public async Task StartAsync(
Stream source,
Stream destination,
int bufferSize,
long totalBytesToExtract,
CancellationToken cancellationToken)
{
lock (_lock)
{
if (_downloadInProgress)
{
// Download already started, no need to attempt
// to start it again.
return;
}

_downloadInProgress = true;
}

await source.CopyToAsync(
destination,
_progressReporter,
bufferSize,
totalBytesToExtract,
cancellationToken);

StopMonitor();
}

public void StopMonitor(string? errorMessage = null)
{
// Send error message to all subscribers.
if (!string.IsNullOrEmpty(errorMessage))
{
PublishProgress(new ByteTransferProgress(errorMessage));
}
else
{
var lastProgress = _lastSentReport.ProgressObject;
PublishProgress(
new ByteTransferProgress(
lastProgress.BytesReceived,
lastProgress.TotalBytesToReceive,
TransferStatus.Succeeded));
}

lock (_lock)
{
// Download already stopped.
if (!_downloadInProgress)
{
return;
}

_downloadInProgress = false;
}
}
}
Loading
Loading