Skip to content

Commit 2110c2a

Browse files
authored
Merge pull request #1656 from GeertvanHorrik/pr/progress-reporting
Add percentage calculation
2 parents b1c0e16 + 42e76b3 commit 2110c2a

8 files changed

+197
-14
lines changed

src/Squirrel/ApplyReleasesProgress.cs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace Squirrel
2+
{
3+
using System;
4+
5+
internal class ApplyReleasesProgress : Progress<int>
6+
{
7+
private readonly int _releasesToApply;
8+
private int _appliedReleases;
9+
private int _currentReleaseProgress;
10+
11+
public ApplyReleasesProgress(int releasesToApply, Action<int> handler)
12+
: base(handler)
13+
{
14+
_releasesToApply = releasesToApply;
15+
}
16+
17+
public void ReportReleaseProgress(int progressOfCurrentRelease)
18+
{
19+
_currentReleaseProgress = progressOfCurrentRelease;
20+
21+
CalculateProgress();
22+
}
23+
24+
public void FinishRelease()
25+
{
26+
_appliedReleases++;
27+
_currentReleaseProgress = 0;
28+
29+
CalculateProgress();
30+
}
31+
32+
private void CalculateProgress()
33+
{
34+
// Per release progress
35+
var perReleaseProgressRange = 100 / _releasesToApply;
36+
37+
var appliedReleases = Math.Min(_appliedReleases, _releasesToApply);
38+
var basePercentage = appliedReleases * perReleaseProgressRange;
39+
40+
var currentReleasePercentage = (perReleaseProgressRange / 100d) * _currentReleaseProgress;
41+
42+
var percentage = basePercentage + currentReleasePercentage;
43+
OnReport((int)percentage);
44+
}
45+
}
46+
}

src/Squirrel/DeltaPackage.cs

+16
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePack
9393
}
9494

9595
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile)
96+
{
97+
return ApplyDeltaPackage(basePackage, deltaPackage, outputFile, x => { });
98+
}
99+
100+
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile, Action<int> progress)
96101
{
97102
Contract.Requires(deltaPackage != null);
98103
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
@@ -108,11 +113,16 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
108113
using (var reader = za.ExtractAllEntries()) {
109114
reader.WriteAllToDirectory(deltaPath, opts);
110115
}
116+
117+
progress(25);
118+
111119
using (var za = ZipArchive.Open(basePackage.InputPackageFile))
112120
using (var reader = za.ExtractAllEntries()) {
113121
reader.WriteAllToDirectory(workingPath, opts);
114122
}
115123

124+
progress(50);
125+
116126
var pathsVisited = new List<string>();
117127

118128
var deltaPathRelativePaths = new DirectoryInfo(deltaPath).GetAllFilesRecursively()
@@ -130,6 +140,8 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
130140
applyDiffToFile(deltaPath, file, workingPath);
131141
});
132142

143+
progress(75);
144+
133145
// Delete all of the files that were in the old package but
134146
// not in the new one.
135147
new DirectoryInfo(workingPath).GetAllFilesRecursively()
@@ -140,6 +152,8 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
140152
File.Delete(Path.Combine(workingPath, x));
141153
});
142154

155+
progress(80);
156+
143157
// Update all the files that aren't in 'lib' with the delta
144158
// package's versions (i.e. the nuspec file, etc etc).
145159
deltaPathRelativePaths
@@ -156,6 +170,8 @@ public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePacka
156170
za.AddAllFromDirectory(workingPath);
157171
za.SaveTo(tgt);
158172
}
173+
174+
progress(100);
159175
}
160176

161177
return new ReleasePackage(outputFile);

src/Squirrel/ReleasePackage.cs

+15
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,26 @@ static Task extractZipWithEscaping(string zipFilePath, string outFolder)
191191
}
192192

193193
public static Task ExtractZipForInstall(string zipFilePath, string outFolder, string rootPackageFolder)
194+
{
195+
return ExtractZipForInstall(zipFilePath, outFolder, rootPackageFolder, x => { });
196+
}
197+
198+
public static Task ExtractZipForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
194199
{
195200
var re = new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
196201

197202
return Task.Run(() => {
198203
using (var za = ZipArchive.Open(zipFilePath))
199204
using (var reader = za.ExtractAllEntries()) {
205+
var totalItems = za.Entries.Count;
206+
var currentItem = 0;
207+
200208
while (reader.MoveToNextEntry()) {
209+
// Report progress early since we might be need to continue for non-matches
210+
currentItem++;
211+
var percentage = (currentItem * 100d) / totalItems;
212+
progress((int)percentage);
213+
201214
var parts = reader.Entry.Key.Split('\\', '/');
202215
var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts);
203216

@@ -234,6 +247,8 @@ public static Task ExtractZipForInstall(string zipFilePath, string outFolder, st
234247
}
235248
}
236249
}
250+
251+
progress(100);
237252
});
238253
}
239254

src/Squirrel/UpdateManager.ApplyReleases.cs

+44-13
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ public async Task<string> ApplyReleases(UpdateInfo updateInfo, bool silentInstal
3232
progress = progress ?? (_ => { });
3333

3434
progress(0);
35-
var release = await createFullPackagesFromDeltas(updateInfo.ReleasesToApply, updateInfo.CurrentlyInstalledVersion);
36-
progress(10);
35+
36+
// Progress range: 00 -> 40
37+
var release = await createFullPackagesFromDeltas(updateInfo.ReleasesToApply, updateInfo.CurrentlyInstalledVersion, new ApplyReleasesProgress(updateInfo.ReleasesToApply.Count, x => progress(CalculateProgress(x, 0, 40))));
38+
39+
progress(40);
3740

3841
if (release == null) {
3942
if (attemptingFullInstall) {
@@ -45,35 +48,46 @@ public async Task<string> ApplyReleases(UpdateInfo updateInfo, bool silentInstal
4548
return getDirectoryForRelease(updateInfo.CurrentlyInstalledVersion.Version).FullName;
4649
}
4750

48-
var ret = await this.ErrorIfThrows(() => installPackageToAppDir(updateInfo, release),
51+
// Progress range: 40 -> 80
52+
var ret = await this.ErrorIfThrows(() => installPackageToAppDir(updateInfo, release, x => progress(CalculateProgress(x, 40, 80))),
4953
"Failed to install package to app dir");
50-
progress(30);
54+
55+
progress(80);
5156

5257
var currentReleases = await this.ErrorIfThrows(() => updateLocalReleasesFile(),
5358
"Failed to update local releases file");
54-
progress(50);
59+
60+
progress(85);
5561

5662
var newVersion = currentReleases.MaxBy(x => x.Version).First().Version;
5763
executeSelfUpdate(newVersion);
5864

65+
progress(90);
66+
5967
await this.ErrorIfThrows(() => invokePostInstall(newVersion, attemptingFullInstall, false, silentInstall),
6068
"Failed to invoke post-install");
61-
progress(75);
69+
70+
progress(95);
6271

6372
this.Log().Info("Starting fixPinnedExecutables");
73+
6474
this.ErrorIfThrows(() => fixPinnedExecutables(updateInfo.FutureReleaseEntry.Version));
6575

76+
progress(96);
77+
6678
this.Log().Info("Fixing up tray icons");
6779

6880
var trayFixer = new TrayStateChanger();
6981
var appDir = new DirectoryInfo(Utility.AppDirForRelease(rootAppDirectory, updateInfo.FutureReleaseEntry));
7082
var allExes = appDir.GetFiles("*.exe").Select(x => x.Name).ToList();
7183

7284
this.ErrorIfThrows(() => trayFixer.RemoveDeadEntries(allExes, rootAppDirectory, updateInfo.FutureReleaseEntry.Version.ToString()));
73-
progress(80);
85+
86+
progress(97);
7487

7588
unshimOurselves();
76-
progress(85);
89+
90+
progress(98);
7791

7892
try {
7993
var currentVersion = updateInfo.CurrentlyInstalledVersion != null ?
@@ -83,6 +97,7 @@ await this.ErrorIfThrows(() => invokePostInstall(newVersion, attemptingFullInsta
8397
} catch (Exception ex) {
8498
this.Log().WarnException("Failed to clean dead versions, continuing anyways", ex);
8599
}
100+
86101
progress(100);
87102

88103
return ret;
@@ -280,7 +295,7 @@ public void RemoveShortcutsForExecutable(string exeName, ShortcutLocation locati
280295
fixPinnedExecutables(zf.Version);
281296
}
282297

283-
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release)
298+
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release, Action<int> progressCallback)
284299
{
285300
return Task.Run(async () => {
286301
var target = getDirectoryForRelease(release.Version);
@@ -297,16 +312,19 @@ Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release)
297312
await ReleasePackage.ExtractZipForInstall(
298313
Path.Combine(updateInfo.PackageDirectory, release.Filename),
299314
target.FullName,
300-
rootAppDirectory);
315+
rootAppDirectory,
316+
progressCallback);
301317

302318
return target.FullName;
303319
});
304320
}
305321

306-
async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry> releasesToApply, ReleaseEntry currentVersion)
322+
async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry> releasesToApply, ReleaseEntry currentVersion, ApplyReleasesProgress progress)
307323
{
308324
Contract.Requires(releasesToApply != null);
309325

326+
progress = progress ?? new ApplyReleasesProgress(releasesToApply.Count(), x => { });
327+
310328
// If there are no remote releases at all, bail
311329
if (!releasesToApply.Any()) {
312330
return null;
@@ -321,6 +339,16 @@ async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry>
321339
throw new Exception("Cannot apply combinations of delta and full packages");
322340
}
323341

342+
// Progress calculation is "complex" here. We need to known how many releases, and then give each release a similar amount of
343+
// progress. For example, when applying 5 releases:
344+
//
345+
// release 1: 00 => 20
346+
// release 2: 20 => 40
347+
// release 3: 40 => 60
348+
// release 4: 60 => 80
349+
// release 5: 80 => 100
350+
//
351+
324352
// Smash together our base full package and the nearest delta
325353
var ret = await Task.Run(() => {
326354
var basePkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename));
@@ -329,9 +357,12 @@ async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry>
329357
var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName);
330358

331359
return deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg,
332-
Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
360+
Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant),
361+
x => progress.ReportReleaseProgress(x));
333362
});
334363

364+
progress.FinishRelease();
365+
335366
if (releasesToApply.Count() == 1) {
336367
return ReleaseEntry.GenerateFromFile(ret.InputPackageFile);
337368
}
@@ -340,7 +371,7 @@ async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry>
340371
var entry = ReleaseEntry.GenerateFromFile(fi.OpenRead(), fi.Name);
341372

342373
// Recursively combine the rest of them
343-
return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry);
374+
return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry, progress);
344375
}
345376

346377
void executeSelfUpdate(SemanticVersion currentVersion)

src/Squirrel/UpdateManager.cs

+21
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,27 @@ Task<IDisposable> acquireUpdateLock()
287287
});
288288
}
289289

290+
/// <summary>
291+
/// Calculates the total percentage of a specific step that should report within a specific range.
292+
/// <para />
293+
/// If a step needs to report between 50 -> 75 %, this method should be used as CalculateProgress(percentage, 50, 75).
294+
/// </summary>
295+
/// <param name="percentageOfCurrentStep">The percentage of the current step, a value between 0 and 100.</param>
296+
/// <param name="stepStartPercentage">The start percentage of the range the current step represents.</param>
297+
/// <param name="stepEndPercentage">The end percentage of the range the current step represents.</param>
298+
/// <returns>The calculated percentage that can be reported about the total progress.</returns>
299+
internal static int CalculateProgress(int percentageOfCurrentStep, int stepStartPercentage, int stepEndPercentage)
300+
{
301+
// Ensure we are between 0 and 100
302+
percentageOfCurrentStep = Math.Max(Math.Min(percentageOfCurrentStep, 100), 0);
303+
304+
var range = stepEndPercentage - stepStartPercentage;
305+
var singleValue = range / 100d;
306+
var totalPercentage = (singleValue * percentageOfCurrentStep) + stepStartPercentage;
307+
308+
return (int)totalPercentage;
309+
}
310+
290311
static string getApplicationName()
291312
{
292313
var fi = new FileInfo(getUpdateExe());

test/ApplyReleasesProgressTests.cs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace Squirrel.Tests
9+
{
10+
public class ApplyReleasesProgressTests
11+
{
12+
13+
[Fact]
14+
public async void CalculatesPercentageCorrectly()
15+
{
16+
// Just 1 complex situation should be enough to cover this
17+
18+
var percentage = 0;
19+
var progress = new ApplyReleasesProgress(5, x => percentage = x);
20+
21+
// 2 releases already finished
22+
progress.FinishRelease();
23+
progress.FinishRelease();
24+
25+
// Report 40 % in current release
26+
progress.ReportReleaseProgress(50);
27+
28+
// Required for callback to be invoked
29+
await Task.Delay(50);
30+
31+
// 20 per release
32+
// 10 because we are half-way the 3rd release
33+
var expectedProgress = 20 + 20 + 10;
34+
35+
Assert.Equal(expectedProgress, percentage);
36+
}
37+
}
38+
}

test/ApplyReleasesTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ public async Task CreateFullPackagesFromDeltaSmokeTest()
437437
var deltaEntry = ReleaseEntry.GenerateFromFile(Path.Combine(tempDir, "theApp", "packages", "Squirrel.Core.1.1.0.0-delta.nupkg"));
438438

439439
var resultObs = (Task<ReleaseEntry>)fixture.GetType().GetMethod("createFullPackagesFromDeltas", BindingFlags.NonPublic | BindingFlags.Instance)
440-
.Invoke(fixture, new object[] { new[] {deltaEntry}, baseEntry });
440+
.Invoke(fixture, new object[] { new[] {deltaEntry}, baseEntry, null });
441441

442442
var result = await resultObs;
443443
var zp = new ZipPackage(Path.Combine(tempDir, "theApp", "packages", result.Filename));

test/UpdateManagerTests.cs

+16
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,22 @@ public void CurrentlyInstalledVersionTests(string input, string expectedVersion)
331331
Assert.Equal(expected, fixture.CurrentlyInstalledVersion(input));
332332
}
333333
}
334+
335+
[Theory]
336+
[InlineData(0, 0, 25, 0)]
337+
[InlineData(12, 0, 25, 3)]
338+
[InlineData(55, 0, 25, 13)]
339+
[InlineData(100, 0, 25, 25)]
340+
[InlineData(0, 25, 50, 25)]
341+
[InlineData(12, 25, 50, 28)]
342+
[InlineData(55, 25, 50, 38)]
343+
[InlineData(100, 25, 50, 50)]
344+
public void CalculatesPercentageCorrectly(int percentageOfCurrentStep, int stepStartPercentage, int stepEndPercentage, int expectedPercentage)
345+
{
346+
var percentage = UpdateManager.CalculateProgress(percentageOfCurrentStep, stepStartPercentage, stepEndPercentage);
347+
348+
Assert.Equal(expectedPercentage, percentage);
349+
}
334350
}
335351
}
336352
}

0 commit comments

Comments
 (0)