Merge pull request #1656 from GeertvanHorrik/pr/progress-reporting

Add percentage calculation
This commit is contained in:
Anaïs Betts
2020-09-27 20:02:46 +02:00
committed by GitHub
8 changed files with 197 additions and 14 deletions

View File

@@ -0,0 +1,46 @@
namespace Squirrel
{
using System;
internal class ApplyReleasesProgress : Progress<int>
{
private readonly int _releasesToApply;
private int _appliedReleases;
private int _currentReleaseProgress;
public ApplyReleasesProgress(int releasesToApply, Action<int> handler)
: base(handler)
{
_releasesToApply = releasesToApply;
}
public void ReportReleaseProgress(int progressOfCurrentRelease)
{
_currentReleaseProgress = progressOfCurrentRelease;
CalculateProgress();
}
public void FinishRelease()
{
_appliedReleases++;
_currentReleaseProgress = 0;
CalculateProgress();
}
private void CalculateProgress()
{
// Per release progress
var perReleaseProgressRange = 100 / _releasesToApply;
var appliedReleases = Math.Min(_appliedReleases, _releasesToApply);
var basePercentage = appliedReleases * perReleaseProgressRange;
var currentReleasePercentage = (perReleaseProgressRange / 100d) * _currentReleaseProgress;
var percentage = basePercentage + currentReleasePercentage;
OnReport((int)percentage);
}
}
}

View File

@@ -93,6 +93,11 @@ namespace Squirrel
}
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile)
{
return ApplyDeltaPackage(basePackage, deltaPackage, outputFile, x => { });
}
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile, Action<int> progress)
{
Contract.Requires(deltaPackage != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
@@ -108,11 +113,16 @@ namespace Squirrel
using (var reader = za.ExtractAllEntries()) {
reader.WriteAllToDirectory(deltaPath, opts);
}
progress(25);
using (var za = ZipArchive.Open(basePackage.InputPackageFile))
using (var reader = za.ExtractAllEntries()) {
reader.WriteAllToDirectory(workingPath, opts);
}
progress(50);
var pathsVisited = new List<string>();
var deltaPathRelativePaths = new DirectoryInfo(deltaPath).GetAllFilesRecursively()
@@ -130,6 +140,8 @@ namespace Squirrel
applyDiffToFile(deltaPath, file, workingPath);
});
progress(75);
// Delete all of the files that were in the old package but
// not in the new one.
new DirectoryInfo(workingPath).GetAllFilesRecursively()
@@ -140,6 +152,8 @@ namespace Squirrel
File.Delete(Path.Combine(workingPath, x));
});
progress(80);
// Update all the files that aren't in 'lib' with the delta
// package's versions (i.e. the nuspec file, etc etc).
deltaPathRelativePaths
@@ -156,6 +170,8 @@ namespace Squirrel
za.AddAllFromDirectory(workingPath);
za.SaveTo(tgt);
}
progress(100);
}
return new ReleasePackage(outputFile);

View File

@@ -191,13 +191,26 @@ namespace Squirrel
}
public static Task ExtractZipForInstall(string zipFilePath, string outFolder, string rootPackageFolder)
{
return ExtractZipForInstall(zipFilePath, outFolder, rootPackageFolder, x => { });
}
public static Task ExtractZipForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
{
var re = new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
return Task.Run(() => {
using (var za = ZipArchive.Open(zipFilePath))
using (var reader = za.ExtractAllEntries()) {
var totalItems = za.Entries.Count;
var currentItem = 0;
while (reader.MoveToNextEntry()) {
// Report progress early since we might be need to continue for non-matches
currentItem++;
var percentage = (currentItem * 100d) / totalItems;
progress((int)percentage);
var parts = reader.Entry.Key.Split('\\', '/');
var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts);
@@ -234,6 +247,8 @@ namespace Squirrel
}
}
}
progress(100);
});
}

View File

@@ -32,8 +32,11 @@ namespace Squirrel
progress = progress ?? (_ => { });
progress(0);
var release = await createFullPackagesFromDeltas(updateInfo.ReleasesToApply, updateInfo.CurrentlyInstalledVersion);
progress(10);
// Progress range: 00 -> 40
var release = await createFullPackagesFromDeltas(updateInfo.ReleasesToApply, updateInfo.CurrentlyInstalledVersion, new ApplyReleasesProgress(updateInfo.ReleasesToApply.Count, x => progress(CalculateProgress(x, 0, 40))));
progress(40);
if (release == null) {
if (attemptingFullInstall) {
@@ -45,24 +48,33 @@ namespace Squirrel
return getDirectoryForRelease(updateInfo.CurrentlyInstalledVersion.Version).FullName;
}
var ret = await this.ErrorIfThrows(() => installPackageToAppDir(updateInfo, release),
// Progress range: 40 -> 80
var ret = await this.ErrorIfThrows(() => installPackageToAppDir(updateInfo, release, x => progress(CalculateProgress(x, 40, 80))),
"Failed to install package to app dir");
progress(30);
progress(80);
var currentReleases = await this.ErrorIfThrows(() => updateLocalReleasesFile(),
"Failed to update local releases file");
progress(50);
progress(85);
var newVersion = currentReleases.MaxBy(x => x.Version).First().Version;
executeSelfUpdate(newVersion);
progress(90);
await this.ErrorIfThrows(() => invokePostInstall(newVersion, attemptingFullInstall, false, silentInstall),
"Failed to invoke post-install");
progress(75);
progress(95);
this.Log().Info("Starting fixPinnedExecutables");
this.ErrorIfThrows(() => fixPinnedExecutables(updateInfo.FutureReleaseEntry.Version));
progress(96);
this.Log().Info("Fixing up tray icons");
var trayFixer = new TrayStateChanger();
@@ -70,10 +82,12 @@ namespace Squirrel
var allExes = appDir.GetFiles("*.exe").Select(x => x.Name).ToList();
this.ErrorIfThrows(() => trayFixer.RemoveDeadEntries(allExes, rootAppDirectory, updateInfo.FutureReleaseEntry.Version.ToString()));
progress(80);
progress(97);
unshimOurselves();
progress(85);
progress(98);
try {
var currentVersion = updateInfo.CurrentlyInstalledVersion != null ?
@@ -83,6 +97,7 @@ namespace Squirrel
} catch (Exception ex) {
this.Log().WarnException("Failed to clean dead versions, continuing anyways", ex);
}
progress(100);
return ret;
@@ -280,7 +295,7 @@ namespace Squirrel
fixPinnedExecutables(zf.Version);
}
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release)
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release, Action<int> progressCallback)
{
return Task.Run(async () => {
var target = getDirectoryForRelease(release.Version);
@@ -297,16 +312,19 @@ namespace Squirrel
await ReleasePackage.ExtractZipForInstall(
Path.Combine(updateInfo.PackageDirectory, release.Filename),
target.FullName,
rootAppDirectory);
rootAppDirectory,
progressCallback);
return target.FullName;
});
}
async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry> releasesToApply, ReleaseEntry currentVersion)
async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry> releasesToApply, ReleaseEntry currentVersion, ApplyReleasesProgress progress)
{
Contract.Requires(releasesToApply != null);
progress = progress ?? new ApplyReleasesProgress(releasesToApply.Count(), x => { });
// If there are no remote releases at all, bail
if (!releasesToApply.Any()) {
return null;
@@ -321,6 +339,16 @@ namespace Squirrel
throw new Exception("Cannot apply combinations of delta and full packages");
}
// Progress calculation is "complex" here. We need to known how many releases, and then give each release a similar amount of
// progress. For example, when applying 5 releases:
//
// release 1: 00 => 20
// release 2: 20 => 40
// release 3: 40 => 60
// release 4: 60 => 80
// release 5: 80 => 100
//
// Smash together our base full package and the nearest delta
var ret = await Task.Run(() => {
var basePkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename));
@@ -329,9 +357,12 @@ namespace Squirrel
var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName);
return deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg,
Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant),
x => progress.ReportReleaseProgress(x));
});
progress.FinishRelease();
if (releasesToApply.Count() == 1) {
return ReleaseEntry.GenerateFromFile(ret.InputPackageFile);
}
@@ -340,7 +371,7 @@ namespace Squirrel
var entry = ReleaseEntry.GenerateFromFile(fi.OpenRead(), fi.Name);
// Recursively combine the rest of them
return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry);
return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry, progress);
}
void executeSelfUpdate(SemanticVersion currentVersion)

View File

@@ -287,6 +287,27 @@ namespace Squirrel
});
}
/// <summary>
/// Calculates the total percentage of a specific step that should report within a specific range.
/// <para />
/// If a step needs to report between 50 -> 75 %, this method should be used as CalculateProgress(percentage, 50, 75).
/// </summary>
/// <param name="percentageOfCurrentStep">The percentage of the current step, a value between 0 and 100.</param>
/// <param name="stepStartPercentage">The start percentage of the range the current step represents.</param>
/// <param name="stepEndPercentage">The end percentage of the range the current step represents.</param>
/// <returns>The calculated percentage that can be reported about the total progress.</returns>
internal static int CalculateProgress(int percentageOfCurrentStep, int stepStartPercentage, int stepEndPercentage)
{
// Ensure we are between 0 and 100
percentageOfCurrentStep = Math.Max(Math.Min(percentageOfCurrentStep, 100), 0);
var range = stepEndPercentage - stepStartPercentage;
var singleValue = range / 100d;
var totalPercentage = (singleValue * percentageOfCurrentStep) + stepStartPercentage;
return (int)totalPercentage;
}
static string getApplicationName()
{
var fi = new FileInfo(getUpdateExe());

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Squirrel.Tests
{
public class ApplyReleasesProgressTests
{
[Fact]
public async void CalculatesPercentageCorrectly()
{
// Just 1 complex situation should be enough to cover this
var percentage = 0;
var progress = new ApplyReleasesProgress(5, x => percentage = x);
// 2 releases already finished
progress.FinishRelease();
progress.FinishRelease();
// Report 40 % in current release
progress.ReportReleaseProgress(50);
// Required for callback to be invoked
await Task.Delay(50);
// 20 per release
// 10 because we are half-way the 3rd release
var expectedProgress = 20 + 20 + 10;
Assert.Equal(expectedProgress, percentage);
}
}
}

View File

@@ -437,7 +437,7 @@ namespace Squirrel.Tests
var deltaEntry = ReleaseEntry.GenerateFromFile(Path.Combine(tempDir, "theApp", "packages", "Squirrel.Core.1.1.0.0-delta.nupkg"));
var resultObs = (Task<ReleaseEntry>)fixture.GetType().GetMethod("createFullPackagesFromDeltas", BindingFlags.NonPublic | BindingFlags.Instance)
.Invoke(fixture, new object[] { new[] {deltaEntry}, baseEntry });
.Invoke(fixture, new object[] { new[] {deltaEntry}, baseEntry, null });
var result = await resultObs;
var zp = new ZipPackage(Path.Combine(tempDir, "theApp", "packages", result.Filename));

View File

@@ -331,6 +331,22 @@ namespace Squirrel.Tests
Assert.Equal(expected, fixture.CurrentlyInstalledVersion(input));
}
}
[Theory]
[InlineData(0, 0, 25, 0)]
[InlineData(12, 0, 25, 3)]
[InlineData(55, 0, 25, 13)]
[InlineData(100, 0, 25, 25)]
[InlineData(0, 25, 50, 25)]
[InlineData(12, 25, 50, 28)]
[InlineData(55, 25, 50, 38)]
[InlineData(100, 25, 50, 50)]
public void CalculatesPercentageCorrectly(int percentageOfCurrentStep, int stepStartPercentage, int stepEndPercentage, int expectedPercentage)
{
var percentage = UpdateManager.CalculateProgress(percentageOfCurrentStep, stepStartPercentage, stepEndPercentage);
Assert.Equal(expectedPercentage, percentage);
}
}
}
}