From d4712dafce4f292bf46f59ea45ea5c7fac0463cc Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 31 Aug 2020 09:33:41 +0200 Subject: [PATCH 1/5] Add percentage calculation --- src/Squirrel/UpdateManager.cs | 21 +++++++++++++++++++++ test/UpdateManagerTests.cs | 16 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/Squirrel/UpdateManager.cs b/src/Squirrel/UpdateManager.cs index f97203d2..f99d688a 100644 --- a/src/Squirrel/UpdateManager.cs +++ b/src/Squirrel/UpdateManager.cs @@ -287,6 +287,27 @@ namespace Squirrel }); } + /// + /// Calculates the total percentage of a specific step that should report within a specific range. + /// + /// If a step needs to report between 50 -> 75 %, this method should be used as CalculateProgress(percentage, 50, 75). + /// + /// The percentage of the current step, a value between 0 and 100. + /// The start percentage of the range the current step represents. + /// The end percentage of the range the current step represents. + /// The calculated percentage that can be reported about the total progress. + 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()); diff --git a/test/UpdateManagerTests.cs b/test/UpdateManagerTests.cs index 00366528..d81593c4 100644 --- a/test/UpdateManagerTests.cs +++ b/test/UpdateManagerTests.cs @@ -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); + } } } } From c4f4294a43cf89bbae51f3e345cb05d0427d1007 Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 31 Aug 2020 09:40:27 +0200 Subject: [PATCH 2/5] Prepare ApplyReleases for improved progress reporting --- src/Squirrel/UpdateManager.ApplyReleases.cs | 37 +++++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index f1cd054d..217193dd 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -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, 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 installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) + Task installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release, Action progressCallback) { return Task.Run(async () => { var target = getDirectoryForRelease(release.Version); @@ -303,7 +318,7 @@ namespace Squirrel }); } - async Task createFullPackagesFromDeltas(IEnumerable releasesToApply, ReleaseEntry currentVersion) + async Task createFullPackagesFromDeltas(IEnumerable releasesToApply, ReleaseEntry currentVersion, Action progressCallback) { Contract.Requires(releasesToApply != null); @@ -340,7 +355,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, progressCallback); } void executeSelfUpdate(SemanticVersion currentVersion) From c2a1db6181e31c20a3e8093bff80919aa190f4a4 Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 31 Aug 2020 10:12:13 +0200 Subject: [PATCH 3/5] Add ApplyReleasesProgress to calculate "complex" multi-release application logic --- src/Squirrel/ApplyReleasesProgress.cs | 46 +++++++++++++++++++++ src/Squirrel/DeltaPackage.cs | 16 +++++++ src/Squirrel/UpdateManager.ApplyReleases.cs | 21 ++++++++-- test/ApplyReleasesProgressTests.cs | 35 ++++++++++++++++ 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 src/Squirrel/ApplyReleasesProgress.cs create mode 100644 test/ApplyReleasesProgressTests.cs diff --git a/src/Squirrel/ApplyReleasesProgress.cs b/src/Squirrel/ApplyReleasesProgress.cs new file mode 100644 index 00000000..bd4962e1 --- /dev/null +++ b/src/Squirrel/ApplyReleasesProgress.cs @@ -0,0 +1,46 @@ +namespace Squirrel +{ + using System; + + internal class ApplyReleasesProgress : Progress + { + private readonly int _releasesToApply; + private int _appliedReleases; + private int _currentReleaseProgress; + + public ApplyReleasesProgress(int releasesToApply, Action 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); + } + } +} diff --git a/src/Squirrel/DeltaPackage.cs b/src/Squirrel/DeltaPackage.cs index 0a50b8fa..1862e36d 100644 --- a/src/Squirrel/DeltaPackage.cs +++ b/src/Squirrel/DeltaPackage.cs @@ -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 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(); 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); diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index 217193dd..6a4c00bb 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -34,7 +34,7 @@ namespace Squirrel progress(0); // Progress range: 00 -> 40 - var release = await createFullPackagesFromDeltas(updateInfo.ReleasesToApply, updateInfo.CurrentlyInstalledVersion, x => progress(CalculateProgress(x, 0, 40))); + var release = await createFullPackagesFromDeltas(updateInfo.ReleasesToApply, updateInfo.CurrentlyInstalledVersion, new ApplyReleasesProgress(updateInfo.ReleasesToApply.Count, x => progress(CalculateProgress(x, 0, 40)))); progress(40); @@ -318,7 +318,7 @@ namespace Squirrel }); } - async Task createFullPackagesFromDeltas(IEnumerable releasesToApply, ReleaseEntry currentVersion, Action progressCallback) + async Task createFullPackagesFromDeltas(IEnumerable releasesToApply, ReleaseEntry currentVersion, ApplyReleasesProgress progress) { Contract.Requires(releasesToApply != null); @@ -336,6 +336,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)); @@ -344,9 +354,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); } @@ -355,7 +368,7 @@ namespace Squirrel var entry = ReleaseEntry.GenerateFromFile(fi.OpenRead(), fi.Name); // Recursively combine the rest of them - return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry, progressCallback); + return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry, progress); } void executeSelfUpdate(SemanticVersion currentVersion) diff --git a/test/ApplyReleasesProgressTests.cs b/test/ApplyReleasesProgressTests.cs new file mode 100644 index 00000000..88c073a4 --- /dev/null +++ b/test/ApplyReleasesProgressTests.cs @@ -0,0 +1,35 @@ +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 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); + + // 20 per release + // 10 because we are half-way the 3rd release + var expectedProgress = 20 + 20 + 10; + + Assert.Equal(expectedProgress, percentage); + } + } +} From 9ffea683d0c64befd7a5e246a544bab33494a8cb Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 31 Aug 2020 10:31:45 +0200 Subject: [PATCH 4/5] Calculate detailed progress for release extraction --- src/Squirrel/ReleasePackage.cs | 15 +++++++++++++++ src/Squirrel/UpdateManager.ApplyReleases.cs | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Squirrel/ReleasePackage.cs b/src/Squirrel/ReleasePackage.cs index 8d156d19..89b7fc1e 100644 --- a/src/Squirrel/ReleasePackage.cs +++ b/src/Squirrel/ReleasePackage.cs @@ -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 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); }); } diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index 6a4c00bb..9c7d8992 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -312,7 +312,8 @@ namespace Squirrel await ReleasePackage.ExtractZipForInstall( Path.Combine(updateInfo.PackageDirectory, release.Filename), target.FullName, - rootAppDirectory); + rootAppDirectory, + progressCallback); return target.FullName; }); From 42e76b334da3dcad310a39252a9f7b5125ace7e1 Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 31 Aug 2020 10:37:59 +0200 Subject: [PATCH 5/5] Fix integration tests --- src/Squirrel/UpdateManager.ApplyReleases.cs | 2 ++ test/ApplyReleasesProgressTests.cs | 5 ++++- test/ApplyReleasesTests.cs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index 9c7d8992..43e51c2e 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -323,6 +323,8 @@ namespace Squirrel { 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; diff --git a/test/ApplyReleasesProgressTests.cs b/test/ApplyReleasesProgressTests.cs index 88c073a4..8749bfcd 100644 --- a/test/ApplyReleasesProgressTests.cs +++ b/test/ApplyReleasesProgressTests.cs @@ -11,7 +11,7 @@ namespace Squirrel.Tests { [Fact] - public void CalculatesPercentageCorrectly() + public async void CalculatesPercentageCorrectly() { // Just 1 complex situation should be enough to cover this @@ -25,6 +25,9 @@ namespace Squirrel.Tests // 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; diff --git a/test/ApplyReleasesTests.cs b/test/ApplyReleasesTests.cs index 65e954b8..0b757ca7 100644 --- a/test/ApplyReleasesTests.cs +++ b/test/ApplyReleasesTests.cs @@ -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)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));