Skip to content

Commit

Permalink
Merge pull request #1656 from GeertvanHorrik/pr/progress-reporting
Browse files Browse the repository at this point in the history
Add percentage calculation
  • Loading branch information
anaisbetts authored Sep 27, 2020
2 parents b1c0e16 + 42e76b3 commit 2110c2a
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 14 deletions.
46 changes: 46 additions & 0 deletions src/Squirrel/ApplyReleasesProgress.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
16 changes: 16 additions & 0 deletions src/Squirrel/DeltaPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePack
}

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));
Expand All @@ -108,11 +113,16 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
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()
Expand All @@ -130,6 +140,8 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
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()
Expand All @@ -140,6 +152,8 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
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
Expand All @@ -156,6 +170,8 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
za.AddAllFromDirectory(workingPath);
za.SaveTo(tgt);
}

progress(100);
}

return new ReleasePackage(outputFile);
Expand Down
15 changes: 15 additions & 0 deletions src/Squirrel/ReleasePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,26 @@ static Task extractZipWithEscaping(string zipFilePath, string outFolder)
}

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);

Expand Down Expand Up @@ -234,6 +247,8 @@ public static Task ExtractZipForInstall(string zipFilePath, string outFolder, st
}
}
}

progress(100);
});
}

Expand Down
57 changes: 44 additions & 13 deletions src/Squirrel/UpdateManager.ApplyReleases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ public async Task<string> ApplyReleases(UpdateInfo updateInfo, bool silentInstal
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) {
Expand All @@ -45,35 +48,46 @@ public async Task<string> ApplyReleases(UpdateInfo updateInfo, bool silentInstal
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();
var appDir = new DirectoryInfo(Utility.AppDirForRelease(rootAppDirectory, updateInfo.FutureReleaseEntry));
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 ?
Expand All @@ -83,6 +97,7 @@ await this.ErrorIfThrows(() => invokePostInstall(newVersion, attemptingFullInsta
} catch (Exception ex) {
this.Log().WarnException("Failed to clean dead versions, continuing anyways", ex);
}

progress(100);

return ret;
Expand Down Expand Up @@ -280,7 +295,7 @@ public void RemoveShortcutsForExecutable(string exeName, ShortcutLocation locati
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);
Expand All @@ -297,16 +312,19 @@ Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release)
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;
Expand All @@ -321,6 +339,16 @@ async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry>
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));
Expand All @@ -329,9 +357,12 @@ async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry>
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);
}
Expand All @@ -340,7 +371,7 @@ async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry>
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)
Expand Down
21 changes: 21 additions & 0 deletions src/Squirrel/UpdateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,27 @@ Task<IDisposable> acquireUpdateLock()
});
}

/// <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());
Expand Down
38 changes: 38 additions & 0 deletions test/ApplyReleasesProgressTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 1 addition & 1 deletion test/ApplyReleasesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ public async Task CreateFullPackagesFromDeltaSmokeTest()
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));
Expand Down
16 changes: 16 additions & 0 deletions test/UpdateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,22 @@ public void CurrentlyInstalledVersionTests(string input, string expectedVersion)
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);
}
}
}
}

0 comments on commit 2110c2a

Please sign in to comment.