Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,12 @@ public async Task<DownloadResult> ExecuteAsync(
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

var (downloaded, elapsed) = await StreamDownloadAsync(stream, fs, totalBytes, existingBytes,
destPath, progress, sw, token).ConfigureAwait(false);
destPath, progress, sw, token, asset.VersionContext).ConfigureAwait(false);

progress?.Report(new DownloadProgress(
Path.GetFileName(destPath), downloaded,
totalBytes > 0 ? totalBytes + existingBytes : null,
100, DownloadStatus.Completed));
100, DownloadStatus.Completed, asset.VersionContext));

return new DownloadResult(asset, destPath, downloaded, elapsed, retries, true, null);
}
Expand Down Expand Up @@ -189,7 +189,8 @@ public async Task<DownloadResult> ExecuteAsync(
/// </remarks>
internal static async Task<(long Downloaded, TimeSpan Elapsed)> StreamDownloadAsync(
Stream source, Stream dest, long totalBytes, long existingBytes,
string destPath, IProgress<DownloadProgress>? progress, Stopwatch sw, CancellationToken token)
string destPath, IProgress<DownloadProgress>? progress, Stopwatch sw, CancellationToken token,
object? versionContext = null)
{
var buffer = new byte[8192];
long downloaded = existingBytes;
Expand All @@ -209,7 +210,7 @@ public async Task<DownloadResult> ExecuteAsync(
progress?.Report(new DownloadProgress(
Path.GetFileName(destPath), downloaded,
totalBytes > 0 ? totalBytes + existingBytes : null,
pct, DownloadStatus.Downloading));
pct, DownloadStatus.Downloading, versionContext));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ public async Task<DownloadResult> ExecuteAsync(
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

var (downloaded, elapsed) = await HttpDownloadExecutor.StreamDownloadAsync(
stream, fs, total, 0, destPath, progress, sw, token).ConfigureAwait(false);
stream, fs, total, 0, destPath, progress, sw, token, asset.VersionContext).ConfigureAwait(false);

progress?.Report(new DownloadProgress(
Path.GetFileName(destPath), downloaded, total > 0 ? total : null, 100, DownloadStatus.Completed));
Path.GetFileName(destPath), downloaded, total > 0 ? total : null, 100,
DownloadStatus.Completed, asset.VersionContext));
return new DownloadResult(asset, destPath, downloaded, elapsed, 0, true, null);
}
catch (Exception ex) when (ex is not OperationCanceledException)
Expand Down
3 changes: 2 additions & 1 deletion src/GeneralUpdate.Core/Download/Models/DownloadAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public record DownloadAsset(
int RecordId = 0,
int? AppType = null,
string? AuthScheme = null,
string? AuthToken = null
string? AuthToken = null,
object? VersionContext = null
)
{
public string? UpdateLog { get; init; }
Expand Down
3 changes: 2 additions & 1 deletion src/GeneralUpdate.Core/Download/Models/DownloadProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public record DownloadProgress(
long BytesDownloaded,
long? TotalBytes,
double Percentage,
DownloadStatus Status
DownloadStatus Status,
object? VersionContext = null
);

public record DownloadResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ namespace GeneralUpdate.Core.Download.Progress;
/// <list type="bullet">
/// <item><term><c>ProgressEventArgs</c></term><description>Fired on every progress report,
/// containing the download percentage, bytes downloaded, and other information.</description></item>
/// <item><term><c>MultiDownloadStatisticsEventArgs</c></term><description>Fired on every progress report
/// with the asset name, byte counts and percentage. Unknown totals use zero; speed and remaining
/// time are unavailable in DownloadProgress and use an empty string and TimeSpan.Zero.</description></item>
/// <item><term><c>MultiDownloadCompletedEventArgs</c></term><description>Fired when the download status is <c>Completed</c>.</description></item>
/// <item><term><c>MultiDownloadErrorEventArgs</c></term><description>Fired when the download status is <c>Failed</c>.</description></item>
/// <item><term><c>MultiAllDownloadCompletedEventArgs</c></term><description>Fired via the static <c>DispatchAllCompleted</c> method when all download tasks are finished.</description></item>
Expand Down Expand Up @@ -57,6 +60,11 @@ public void Report(Models.DownloadProgress value)

// Fire progress event via EventManager
EventManager.Instance.Dispatch(this, new ProgressEventArgs(value));
var version = value.VersionContext ?? value.AssetName ?? "unknown";
EventManager.Instance.Dispatch(this,
new MultiDownloadStatisticsEventArgs(version,
TimeSpan.Zero, string.Empty, value.TotalBytes ?? 0,
value.BytesDownloaded, value.Percentage));

if (value.Status == Models.DownloadStatus.Completed)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ private static DownloadAsset MapVersionEntry(VersionEntry v)
RecordId: v.RecordId,
AppType: v.AppType,
AuthScheme: v.AuthScheme,
AuthToken: v.AuthToken
AuthToken: v.AuthToken,
VersionContext: v
)
{
UpdateLog = v.UpdateLog
Expand Down
5 changes: 3 additions & 2 deletions src/GeneralUpdate.Core/Strategy/ClientStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -573,17 +573,18 @@ private async Task ExecuteStandardWorkflowAsync()

async Task<Download.Abstractions.DownloadReport> ExecuteDownloadAsync(Download.Models.DownloadPlan plan)
{
var progress = Download.Progress.DownloadProgressReporter.CreateEventBridge();
if (_orchestrator != null)
{
return await _orchestrator.ExecuteAsync(plan, _configInfo.TempPath).ConfigureAwait(false);
return await _orchestrator.ExecuteAsync(plan, _configInfo.TempPath, progress: progress).ConfigureAwait(false);
}
else
{
var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared;
var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(
httpClient, orchOptions, _customDownloadPolicy,
_customDownloadExecutor, _customDownloadPipelineFactory);
return await orchestrator.ExecuteAsync(plan, _configInfo.TempPath).ConfigureAwait(false);
return await orchestrator.ExecuteAsync(plan, _configInfo.TempPath, progress: progress).ConfigureAwait(false);
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/GeneralUpdate.Core/Strategy/OssStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,10 @@ private static bool IsOssUpgrade(string clientVersion, string serverVersion)
private async Task DownloadAssetsAsync(List<DownloadAsset> assets, string targetPath)
{
var plan = new DownloadPlan(assets, false);
var progress = Download.Progress.DownloadProgressReporter.CreateEventBridge();
if (DownloadOrchestrator != null)
{
await DownloadOrchestrator.ExecuteAsync(plan, targetPath).ConfigureAwait(false);
await DownloadOrchestrator.ExecuteAsync(plan, targetPath, progress: progress).ConfigureAwait(false);
}
else
{
Expand All @@ -533,7 +534,7 @@ private async Task DownloadAssetsAsync(List<DownloadAsset> assets, string target
};
var orchestrator = new DefaultDownloadOrchestrator(
Network.HttpClientProvider.Shared, options);
await orchestrator.ExecuteAsync(plan, targetPath).ConfigureAwait(false);
await orchestrator.ExecuteAsync(plan, targetPath, progress: progress).ConfigureAwait(false);
}
}

Expand Down
41 changes: 41 additions & 0 deletions tests/CoreTest/Download/DownloadProgressReporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,47 @@ public void Dispose()

#region Progress callback

[Theory]
[InlineData(1000L, 50.0, DownloadStatus.Downloading)]
[InlineData(null, -1.0, DownloadStatus.Downloading)]
[InlineData(500L, 100.0, DownloadStatus.Completed)]
public void Report_DispatchesStatisticsAlongsideProgress(long? totalBytes, double percentage, DownloadStatus status)
{
MultiDownloadStatisticsEventArgs? statistics = null;
ProgressEventArgs? progressArgs = null;
var completedCount = 0;
new GeneralUpdate.Core.GeneralUpdateBootstrap()
.AddListenerMultiDownloadStatistics((_, args) => statistics = args)
.AddListenerProgress((_, args) => progressArgs = args)
.AddListenerMultiDownloadCompleted((_, _) => completedCount++);
var progress = new DownloadProgress("asset.zip", 500, totalBytes, percentage, status);

DownloadProgressReporter.CreateEventBridge().Report(progress);

Assert.NotNull(statistics);
Assert.Equal("asset.zip", statistics.Version);
Assert.Equal(totalBytes ?? 0, statistics.TotalBytesToReceive);
Assert.Equal(500, statistics.BytesReceived);
Assert.Equal(percentage, statistics.ProgressPercentage);
Assert.Same(progress, progressArgs?.Progress);
Assert.Equal(status == DownloadStatus.Completed ? 1 : 0, completedCount);
}

[Fact]
public void Report_StatisticsUsesVersionContextWhenProvided()
{
MultiDownloadStatisticsEventArgs? statistics = null;
new GeneralUpdate.Core.GeneralUpdateBootstrap()
.AddListenerMultiDownloadStatistics((_, args) => statistics = args);
var version = new GeneralUpdate.Core.Configuration.VersionEntry { Version = "1.2.3" };
var progress = new DownloadProgress("asset.zip", 500, 1000, 50.0, DownloadStatus.Downloading, version);

DownloadProgressReporter.CreateEventBridge().Report(progress);

Assert.NotNull(statistics);
Assert.Same(version, statistics!.Version);
}

[Fact]
public void Report_InvokesOnProgressCallback()
{
Expand Down
118 changes: 118 additions & 0 deletions tests/CoreTest/Strategy/DownloadEventTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System.Collections.Concurrent;
using System.Reflection;
using GeneralUpdate.Core;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Download;
using GeneralUpdate.Core.Download.Abstractions;
using GeneralUpdate.Core.Download.Models;
using GeneralUpdate.Core.Download.Orchestrators;
using GeneralUpdate.Core.Event;
using GeneralUpdate.Core.FileSystem;
using GeneralUpdate.Core.Strategy;
using Moq;

namespace CoreTest.Strategy;

[Collection("NonParallel_EventManager")]
public class DownloadEventTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"GU_DownloadEvents_{Guid.NewGuid():N}");
private readonly ConcurrentQueue<MultiDownloadStatisticsEventArgs> _statistics = new();
private readonly ConcurrentQueue<MultiDownloadCompletedEventArgs> _completed = new();
private readonly ConcurrentQueue<MultiAllDownloadCompletedEventArgs> _allCompleted = new();
private readonly IBlackMatcher? _blackMatcher = StorageManager.BlackMatcher;

public DownloadEventTests()
{
Directory.CreateDirectory(_directory);
new GeneralUpdateBootstrap()
.AddListenerMultiDownloadStatistics((_, args) => _statistics.Enqueue(args))
.AddListenerMultiDownloadCompleted((_, args) => _completed.Enqueue(args))
.AddListenerMultiAllDownloadCompleted((_, args) => _allCompleted.Enqueue(args));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ClientDownload_DefaultAndInjectedOrchestrators_NotifyListeners(bool injectOrchestrator)
{
var asset = new DownloadAsset("update.zip", "https://example.com/update.zip",
1000, null, "2.0.0", AppType: (int)AppType.Upgrade);
var source = new Mock<IDownloadSource>();
source.Setup(s => s.ListAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new DownloadSourceResult { Assets = new[] { asset }, HasUpgradeUpdate = true });
var executor = CreateExecutor();
using var httpClient = new HttpClient();
var strategy = new ClientStrategy { DownloadSource = source.Object };
strategy.SetOsStrategy(Mock.Of<IStrategy>());
if (injectOrchestrator)
strategy.SetOrchestrator(new DefaultDownloadOrchestrator(httpClient,
new DownloadOrchestratorOptions { VerifyChecksum = false }, executor: executor.Object));
else
strategy.SetDownloadExecutor(executor.Object);
var context = new UpdateContext
{
UpdateUrl = "https://example.com/update",
ClientVersion = "1.0.0",
UpgradeClientVersion = "1.0.0",
MainAppName = "MainApp",
InstallPath = _directory,
BackupEnabled = false,
VerifyChecksum = false
};
strategy.Create(context);

await strategy.ExecuteAsync();

AssertDownloadEvents();
}

[Fact]
public async Task OssDownload_InjectedOrchestrator_NotifiesListeners()
{
var asset = new DownloadAsset("update.zip", "https://example.com/update.zip", 1000, null, "2.0.0");
using var httpClient = new HttpClient();
var strategy = new OssStrategy(AppType.OssUpgrade)
{
DownloadOrchestrator = new DefaultDownloadOrchestrator(httpClient,
new DownloadOrchestratorOptions { VerifyChecksum = false }, executor: CreateExecutor().Object)
};
// Exercise the download phase without launching applications or exiting the test process.
var download = typeof(OssStrategy).GetMethod("DownloadAssetsAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;

await (Task)download.Invoke(strategy, new object[] { new List<DownloadAsset> { asset }, _directory })!;

AssertDownloadEvents();
}

private static Mock<IDownloadExecutor> CreateExecutor()
{
var executor = new Mock<IDownloadExecutor>();
executor.Setup(e => e.ExecuteAsync(It.IsAny<DownloadAsset>(), It.IsAny<string>(),
It.IsAny<IProgress<DownloadProgress>>(), It.IsAny<CancellationToken>()))
.Returns((DownloadAsset asset, string path, IProgress<DownloadProgress>? progress, CancellationToken _) =>
{
progress?.Report(new DownloadProgress(asset.Name, 500, 1000, 50, DownloadStatus.Downloading));
progress?.Report(new DownloadProgress(asset.Name, 1000, 1000, 100, DownloadStatus.Completed));
return Task.FromResult(new DownloadResult(asset, path, 1000, TimeSpan.Zero, 0, true, null));
});
return executor;
}

private void AssertDownloadEvents()
{
var completed = Assert.Single(_completed);
Assert.True(completed.IsCompleted);
Assert.Equal("update.zip", completed.Version);
Assert.Contains(_statistics, s => s.BytesReceived == 500 && s.ProgressPercentage == 50);
Assert.Contains(_statistics, s => s.BytesReceived == 1000 && s.ProgressPercentage == 100);
Assert.True(Assert.Single(_allCompleted).IsAllDownloadCompleted);
}

public void Dispose()
{
EventManager.Instance.Clear();
StorageManager.BlackMatcher = _blackMatcher;
Directory.Delete(_directory, true);
}
}
Loading