From f20ed65acb93a2487ddc499dcdc1adb5f122a826 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 12 Apr 2016 12:47:59 -0700 Subject: [PATCH 01/16] Add StagingPercentage to ReleaseEntry --- src/Squirrel/ReleaseEntry.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index 70f86c20a..90b728a44 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -22,6 +22,7 @@ public interface IReleaseEntry string EntryAsString { get; } SemanticVersion Version { get; } string PackageName { get; } + float? StagingPercentage { get; } string GetReleaseNotes(string packageDirectory); Uri GetIconUrl(string packageDirectory); @@ -36,15 +37,16 @@ public class ReleaseEntry : IEnableLogger, IReleaseEntry [DataMember] public string Query { get; protected set; } [DataMember] public long Filesize { get; protected set; } [DataMember] public bool IsDelta { get; protected set; } + [DataMember] public float? StagingPercentage { get; protected set; } - protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta, string baseUrl = null, string query = null) + protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta, string baseUrl = null, string query = null, float? stagingPercentage = null) { Contract.Requires(sha1 != null && sha1.Length == 40); Contract.Requires(filename != null); Contract.Requires(filename.Contains(Path.DirectorySeparatorChar) == false); Contract.Requires(filesize > 0); - SHA1 = sha1; BaseUrl = baseUrl; Filename = filename; Query = query; Filesize = filesize; IsDelta = isDelta; + SHA1 = sha1; BaseUrl = baseUrl; Filename = filename; Query = query; Filesize = filesize; IsDelta = isDelta; StagingPercentage = stagingPercentage; } [IgnoreDataMember] From 2c17638612e833ee440374ed209b3ca39a779ca9 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 12 Apr 2016 12:48:36 -0700 Subject: [PATCH 02/16] Handle serializing ReleaseEntries --- src/Squirrel/ReleaseEntry.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index 90b728a44..454b2ddf7 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -51,7 +51,13 @@ protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta [IgnoreDataMember] public string EntryAsString { - get { return String.Format("{0} {1}{2} {3}", SHA1, BaseUrl, Filename, Filesize); } + get { + if (StagingPercentage != null) { + return String.Format("{0} {1}{2} {3} # ${4}", SHA1, BaseUrl, Filename, Filesize, stagingPercentageAsString(StagingPercentage.Value)); + } else { + return String.Format("{0} {1}{2} {3}", SHA1, BaseUrl, Filename, Filesize); + } + } } [IgnoreDataMember] @@ -227,6 +233,11 @@ public static List BuildReleasesFile(string releasePackagesDir) return entries; } + static string stagingPercentageAsString(float percentage) + { + return String.Format("{0:F0}%", percentage * 100.0)); + } + static bool filenameIsDeltaFile(string filename) { return filename.EndsWith("-delta.nupkg", StringComparison.InvariantCultureIgnoreCase); From a13cc0c312ea6d51e3677f79c0928d5581fad10e Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 11:53:26 -0700 Subject: [PATCH 03/16] WIP --- src/Squirrel/ReleaseEntry.cs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index 454b2ddf7..f343671a6 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -88,16 +88,23 @@ public Uri GetIconUrl(string packageDirectory) static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$"); static readonly Regex commentRegex = new Regex(@"#.*$"); + static readonly Regex stagingRegex = new Regex(@"#\s+(\d{1,3})%$"); public static ReleaseEntry ParseReleaseEntry(string entry) { Contract.Requires(entry != null); + float? stagingPercentage = null; + var m = stagingRegex.Match(entry); + if (m != null) { + stagingPercentage = Single.Parse(m.Groups[0].Value); + } + entry = commentRegex.Replace(entry, ""); if (String.IsNullOrWhiteSpace(entry)) { return null; } - var m = entryRegex.Match(entry); + m = entryRegex.Match(entry); if (!m.Success) { throw new Exception("Invalid release entry: " + entry); } @@ -138,7 +145,7 @@ public static ReleaseEntry ParseReleaseEntry(string entry) long size = Int64.Parse(m.Groups[3].Value); bool isDelta = filenameIsDeltaFile(filename); - return new ReleaseEntry(m.Groups[1].Value, filename, size, isDelta, baseUrl, query); + return new ReleaseEntry(m.Groups[1].Value, filename, size, isDelta, baseUrl, query, stagingPercentage); } public static IEnumerable ParseReleaseFile(string fileContents) @@ -158,6 +165,24 @@ public static IEnumerable ParseReleaseFile(string fileContents) return ret.Any(x => x == null) ? null : ret; } + public static IEnumerable ParseReleaseFileAndApplyStaging(string fileContents, Guid userToken) + { + if (String.IsNullOrEmpty(fileContents)) { + return new ReleaseEntry[0]; + } + + fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents); + + var ret = fileContents.Split('\n') + .Where(x => !String.IsNullOrWhiteSpace(x)) + .Select(ParseReleaseEntry) + .Where(x => x != null) + .ToArray(); + + return ret.Any(x => x == null) ? null : ret; + } + + public static void WriteReleaseFile(IEnumerable releaseEntries, Stream stream) { Contract.Requires(releaseEntries != null && releaseEntries.Any()); @@ -235,7 +260,7 @@ public static List BuildReleasesFile(string releasePackagesDir) static string stagingPercentageAsString(float percentage) { - return String.Format("{0:F0}%", percentage * 100.0)); + return String.Format("{0:F0}%", percentage * 100.0); } static bool filenameIsDeltaFile(string filename) From 14f3a0ac2377c4f11c7f38171494f9edfc39e89a Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 12:08:12 -0700 Subject: [PATCH 04/16] This is not JavaScript --- src/Squirrel/ReleaseEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index f343671a6..c4f22b198 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -95,7 +95,7 @@ public static ReleaseEntry ParseReleaseEntry(string entry) float? stagingPercentage = null; var m = stagingRegex.Match(entry); - if (m != null) { + if (m != null && m.Success) { stagingPercentage = Single.Parse(m.Groups[0].Value); } From 486c2d5e3f32dffe3da971d7b25e932e6063db64 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 12:22:18 -0700 Subject: [PATCH 05/16] So much TDD Lyfe --- test/ReleaseEntryTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/ReleaseEntryTests.cs b/test/ReleaseEntryTests.cs index 3e0c3817c..0a63362ae 100644 --- a/test/ReleaseEntryTests.cs +++ b/test/ReleaseEntryTests.cs @@ -88,6 +88,26 @@ public void ParseVersionTest(string releaseEntry, int major, int minor, int patc Assert.Equal(isDelta, fixture.IsDelta); } + [Theory] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.nupkg 123 # 10%", 1, 2, 0, 0, "", false, 0.1f)] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-full.nupkg 123 # 90%", 1, 2, 0, 0, "", false, 0.9f)] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", 1, 2, 0, 0, "", true, null)] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123 # 5%", 1, 2, 0, 0, "", true, 0.05f)] + public void ParseStagingPercentageTest(string releaseEntry, int major, int minor, int patch, int revision, string prerelease, bool isDelta, float? stagingPercentage) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + + Assert.Equal(new SemanticVersion(new Version(major, minor, patch, revision), prerelease), fixture.Version); + Assert.Equal(isDelta, fixture.IsDelta); + + if (stagingPercentage.HasValue) { + Assert.True(Math.Abs(fixture.StagingPercentage.Value - stagingPercentage.Value) < 0.001); + } else { + Assert.Null(fixture.StagingPercentage); + } + } + + [Fact] public void CanParseGeneratedReleaseEntryAsString() { From ac76c0da21009787e8a7a6f7032a919146203f49 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 12:42:45 -0700 Subject: [PATCH 06/16] Hey this testing thing might be not that bad --- src/Squirrel/ReleaseEntry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index c4f22b198..405cfa56b 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -87,7 +87,7 @@ public Uri GetIconUrl(string packageDirectory) } static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$"); - static readonly Regex commentRegex = new Regex(@"#.*$"); + static readonly Regex commentRegex = new Regex(@"\s*#.*$"); static readonly Regex stagingRegex = new Regex(@"#\s+(\d{1,3})%$"); public static ReleaseEntry ParseReleaseEntry(string entry) { @@ -96,7 +96,7 @@ public static ReleaseEntry ParseReleaseEntry(string entry) float? stagingPercentage = null; var m = stagingRegex.Match(entry); if (m != null && m.Success) { - stagingPercentage = Single.Parse(m.Groups[0].Value); + stagingPercentage = Single.Parse(m.Groups[1].Value) / 100.0f; } entry = commentRegex.Replace(entry, ""); From f7df740f3bbe114580478952b506e38eb0a86ad3 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 12:43:00 -0700 Subject: [PATCH 07/16] Let us make GUIDs from bytes --- src/Squirrel/Utility.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Squirrel/Utility.cs b/src/Squirrel/Utility.cs index 74df2539f..54b0c0fa5 100644 --- a/src/Squirrel/Utility.cs +++ b/src/Squirrel/Utility.cs @@ -589,14 +589,18 @@ public static Guid CreateGuidFromHash(string text) { return CreateGuidFromHash(text, Utility.IsoOidNamespace); } - + public static Guid CreateGuidFromHash(byte[] data) + { + return CreateGuidFromHash(data, Utility.IsoOidNamespace); + } public static Guid CreateGuidFromHash(string text, Guid namespaceId) { - // convert the name to a sequence of octets (as defined by the standard - // or conventions of its namespace) (step 3) - byte[] nameBytes = Encoding.UTF8.GetBytes(text); + return CreateGuidFromHash(Encoding.UTF8.GetBytes(text), namespaceId); + } + public static Guid CreateGuidFromHash(byte[] nameBytes, Guid namespaceId) + { // convert the namespace UUID to network order (step 3) byte[] namespaceBytes = namespaceId.ToByteArray(); SwapByteOrder(namespaceBytes); From 0369f5b734c2039725101eeb5dcdc4ffaa622559 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 12:43:37 -0700 Subject: [PATCH 08/16] Create a persistent User ID that is just a random identifier --- .../UpdateManager.DownloadReleases.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Squirrel/UpdateManager.DownloadReleases.cs b/src/Squirrel/UpdateManager.DownloadReleases.cs index e45e64971..b623d5762 100644 --- a/src/Squirrel/UpdateManager.DownloadReleases.cs +++ b/src/Squirrel/UpdateManager.DownloadReleases.cs @@ -109,6 +109,35 @@ void checksumPackage(ReleaseEntry downloadedRelease) } } } + + internal Guid? getOrCreateStagedUserId() + { + var stagedUserIdFile = Path.Combine(rootAppDirectory, "packages", ".betaId"); + var ret = default(Guid); + + try { + if (!Guid.TryParse(File.ReadAllText(stagedUserIdFile, Encoding.UTF8), out ret)) { + throw new Exception("File was read but contents were invalid"); + } + + return ret; + } catch (Exception ex) { + this.Log().InfoException("Couldn't read staging user ID, creating a blank one", ex); + } + + var prng = new Random(); + var buf = new byte[4096]; + prng.NextBytes(buf); + + ret = Utility.CreateGuidFromHash(buf); + try { + File.WriteAllText(stagedUserIdFile, ret.ToString(), Encoding.UTF8); + return ret; + } catch (Exception ex) { + this.Log().WarnException("Couldn't write out staging user ID, this user probably shouldn't get beta anything", ex); + return null; + } + } } } } From fcaaecdb1ef3979399300a773d0422887155e6f4 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 13:25:01 -0700 Subject: [PATCH 09/16] What's that dollar sign doing there --- src/Squirrel/ReleaseEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index 405cfa56b..696b4ffb7 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -53,7 +53,7 @@ protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta public string EntryAsString { get { if (StagingPercentage != null) { - return String.Format("{0} {1}{2} {3} # ${4}", SHA1, BaseUrl, Filename, Filesize, stagingPercentageAsString(StagingPercentage.Value)); + return String.Format("{0} {1}{2} {3} # {4}", SHA1, BaseUrl, Filename, Filesize, stagingPercentageAsString(StagingPercentage.Value)); } else { return String.Format("{0} {1}{2} {3}", SHA1, BaseUrl, Filename, Filesize); } From 40eb2bf7308579d326d0d768f7e8e5ec9ffbcd0a Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 13:25:38 -0700 Subject: [PATCH 10/16] Implement matching on staging --- src/Squirrel/ReleaseEntry.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index 696b4ffb7..0e94359bd 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -148,6 +148,17 @@ public static ReleaseEntry ParseReleaseEntry(string entry) return new ReleaseEntry(m.Groups[1].Value, filename, size, isDelta, baseUrl, query, stagingPercentage); } + public bool IsStagingMatch(Guid? userId) + { + if (!StagingPercentage.HasValue) return true; + if (!userId.HasValue) return false; + + uint val = BitConverter.ToUInt32(userId.Value.ToByteArray(), 12); + + double percentage = ((double)val / (double)UInt32.MaxValue); + return percentage < StagingPercentage.Value; + } + public static IEnumerable ParseReleaseFile(string fileContents) { if (String.IsNullOrEmpty(fileContents)) { @@ -165,7 +176,7 @@ public static IEnumerable ParseReleaseFile(string fileContents) return ret.Any(x => x == null) ? null : ret; } - public static IEnumerable ParseReleaseFileAndApplyStaging(string fileContents, Guid userToken) + public static IEnumerable ParseReleaseFileAndApplyStaging(string fileContents, Guid? userToken) { if (String.IsNullOrEmpty(fileContents)) { return new ReleaseEntry[0]; @@ -176,7 +187,7 @@ public static IEnumerable ParseReleaseFileAndApplyStaging(string f var ret = fileContents.Split('\n') .Where(x => !String.IsNullOrWhiteSpace(x)) .Select(ParseReleaseEntry) - .Where(x => x != null) + .Where(x => x != null && x.IsStagingMatch(userToken)) .ToArray(); return ret.Any(x => x == null) ? null : ret; From 91c719f040dc8f37cff8448795575ebc1bb14e15 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 13:25:53 -0700 Subject: [PATCH 11/16] Write some tests so we believe it --- test/ReleaseEntryTests.cs | 93 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/test/ReleaseEntryTests.cs b/test/ReleaseEntryTests.cs index 0a63362ae..c9fd6aa91 100644 --- a/test/ReleaseEntryTests.cs +++ b/test/ReleaseEntryTests.cs @@ -238,6 +238,90 @@ public void WhenReleasesAreOutOfOrderSortByVersion() Assert.Equal(false, releases[4].IsDelta); } + [Fact] + public void StagingUsersGetBetaSoftware() + { + // NB: We're kind of using a hack here, in that we know that the + // last 4 bytes are used as the percentage, and the percentage + // effectively measures, "How close are you to zero". Guid.Empty + // is v close to zero, because it is zero. + var path = Path.GetTempFileName(); + var ourGuid = Guid.Empty; + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(3, releases.Length); + } + + [Fact] + public void BorkedUsersGetProductionSoftware() + { + var path = Path.GetTempFileName(); + var ourGuid = default(Guid?); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(2, releases.Length); + } + + [Theory] + [InlineData("{22b29e6f-bd2e-43d2-85ca-ffffffffffff}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-888888888888}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-444444444444}")] + public void UnluckyUsersGetProductionSoftware(string inputGuid) + { + var path = Path.GetTempFileName(); + var ourGuid = Guid.ParseExact(inputGuid, "B"); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(2, releases.Length); + } + + [Theory] + [InlineData("{22b29e6f-bd2e-43d2-85ca-333333333333}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-111111111111}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-000000000000}")] + public void LuckyUsersGetBetaSoftware(string inputGuid) + { + var path = Path.GetTempFileName(); + var ourGuid = Guid.ParseExact(inputGuid, "B"); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.25f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(3, releases.Length); + } + + + [Fact] public void ParseReleaseFileShouldReturnNothingForBlankFiles() { @@ -245,9 +329,14 @@ public void ParseReleaseFileShouldReturnNothingForBlankFiles() Assert.True(ReleaseEntry.ParseReleaseFile(null).Count() == 0); } - static string MockReleaseEntry(string name) + static string MockReleaseEntry(string name, float? percentage = null) { - return string.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502", name); + if (percentage.HasValue) { + var ret = String.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502 # {1:F0}%", name, percentage * 100.0f); + return ret; + } else { + return String.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502", name); + } } } } From 63ea62c3be3d4d641099b371216e81b50f938a37 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 14:20:09 -0700 Subject: [PATCH 12/16] Wire everything up --- src/Squirrel/UpdateManager.CheckForUpdates.cs | 34 ++++++++++++++++++- .../UpdateManager.DownloadReleases.cs | 28 --------------- test/ReleaseEntryTests.cs | 2 -- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/Squirrel/UpdateManager.CheckForUpdates.cs b/src/Squirrel/UpdateManager.CheckForUpdates.cs index 42c621d00..c4030fc3b 100644 --- a/src/Squirrel/UpdateManager.CheckForUpdates.cs +++ b/src/Squirrel/UpdateManager.CheckForUpdates.cs @@ -30,6 +30,7 @@ public async Task CheckForUpdate( progress = progress ?? (_ => { }); var localReleases = Enumerable.Empty(); + var stagingId = getOrCreateStagedUserId(); bool shouldInitialize = false; try { @@ -117,7 +118,7 @@ public async Task CheckForUpdate( } var ret = default(UpdateInfo); - var remoteReleases = ReleaseEntry.ParseReleaseFile(releaseFile); + var remoteReleases = ReleaseEntry.ParseReleaseFileAndApplyStaging(releaseFile, stagingId); progress(66); if (!remoteReleases.Any()) { @@ -177,6 +178,37 @@ UpdateInfo determineUpdateInfo(IEnumerable localReleases, IEnumera return UpdateInfo.Create(currentRelease, remoteReleases, packageDirectory); } + + internal Guid? getOrCreateStagedUserId() + { + var stagedUserIdFile = Path.Combine(rootAppDirectory, "packages", ".betaId"); + var ret = default(Guid); + + try { + if (!Guid.TryParse(File.ReadAllText(stagedUserIdFile, Encoding.UTF8), out ret)) { + throw new Exception("File was read but contents were invalid"); + } + + this.Log().Info("Using existing staging user ID: {0}", ret.ToString()); + return ret; + } catch (Exception ex) { + this.Log().InfoException("Couldn't read staging user ID, creating a blank one", ex); + } + + var prng = new Random(); + var buf = new byte[4096]; + prng.NextBytes(buf); + + ret = Utility.CreateGuidFromHash(buf); + try { + File.WriteAllText(stagedUserIdFile, ret.ToString(), Encoding.UTF8); + this.Log().Info("Generated new staging user ID: {0}", ret.ToString()); + return ret; + } catch (Exception ex) { + this.Log().WarnException("Couldn't write out staging user ID, this user probably shouldn't get beta anything", ex); + return null; + } + } } } } diff --git a/src/Squirrel/UpdateManager.DownloadReleases.cs b/src/Squirrel/UpdateManager.DownloadReleases.cs index b623d5762..a01d37855 100644 --- a/src/Squirrel/UpdateManager.DownloadReleases.cs +++ b/src/Squirrel/UpdateManager.DownloadReleases.cs @@ -110,34 +110,6 @@ void checksumPackage(ReleaseEntry downloadedRelease) } } - internal Guid? getOrCreateStagedUserId() - { - var stagedUserIdFile = Path.Combine(rootAppDirectory, "packages", ".betaId"); - var ret = default(Guid); - - try { - if (!Guid.TryParse(File.ReadAllText(stagedUserIdFile, Encoding.UTF8), out ret)) { - throw new Exception("File was read but contents were invalid"); - } - - return ret; - } catch (Exception ex) { - this.Log().InfoException("Couldn't read staging user ID, creating a blank one", ex); - } - - var prng = new Random(); - var buf = new byte[4096]; - prng.NextBytes(buf); - - ret = Utility.CreateGuidFromHash(buf); - try { - File.WriteAllText(stagedUserIdFile, ret.ToString(), Encoding.UTF8); - return ret; - } catch (Exception ex) { - this.Log().WarnException("Couldn't write out staging user ID, this user probably shouldn't get beta anything", ex); - return null; - } - } } } } diff --git a/test/ReleaseEntryTests.cs b/test/ReleaseEntryTests.cs index c9fd6aa91..3037aa715 100644 --- a/test/ReleaseEntryTests.cs +++ b/test/ReleaseEntryTests.cs @@ -320,8 +320,6 @@ public void LuckyUsersGetBetaSoftware(string inputGuid) Assert.Equal(3, releases.Length); } - - [Fact] public void ParseReleaseFileShouldReturnNothingForBlankFiles() { From 7b294a9c697c5602adb87360cc9d8cf3f2ceddf9 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Tue, 19 Apr 2016 14:45:55 -0700 Subject: [PATCH 13/16] UGH --- src/Squirrel/UpdateManager.DownloadReleases.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Squirrel/UpdateManager.DownloadReleases.cs b/src/Squirrel/UpdateManager.DownloadReleases.cs index a01d37855..e45e64971 100644 --- a/src/Squirrel/UpdateManager.DownloadReleases.cs +++ b/src/Squirrel/UpdateManager.DownloadReleases.cs @@ -109,7 +109,6 @@ void checksumPackage(ReleaseEntry downloadedRelease) } } } - } } } From e7aaab5f84321b4e1800b230ebc30631382ca585 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Wed, 20 Apr 2016 10:32:08 -0700 Subject: [PATCH 14/16] Code Reviews --- src/Squirrel/ReleaseEntry.cs | 7 +++++-- src/Squirrel/UpdateManager.CheckForUpdates.cs | 18 +++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Squirrel/ReleaseEntry.cs b/src/Squirrel/ReleaseEntry.cs index 0e94359bd..855c82dd7 100644 --- a/src/Squirrel/ReleaseEntry.cs +++ b/src/Squirrel/ReleaseEntry.cs @@ -115,7 +115,7 @@ public static ReleaseEntry ParseReleaseEntry(string entry) string filename = m.Groups[2].Value; - // Split the base URL and the filename if an URI is provided, + // Split the base URL and the filename if an URI is provided, // throws if a path is provided string baseUrl = null; string query = null; @@ -137,7 +137,7 @@ public static ReleaseEntry ParseReleaseEntry(string entry) query = uri.Query; } } - + if (filename.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) { throw new Exception("Filename can either be an absolute HTTP[s] URL, *or* a file name"); } @@ -150,6 +150,9 @@ public static ReleaseEntry ParseReleaseEntry(string entry) public bool IsStagingMatch(Guid? userId) { + // A "Staging match" is when a user falls into the affirmative + // bucket - i.e. if the staging is at 10%, this user is the one out + // of ten case. if (!StagingPercentage.HasValue) return true; if (!userId.HasValue) return false; diff --git a/src/Squirrel/UpdateManager.CheckForUpdates.cs b/src/Squirrel/UpdateManager.CheckForUpdates.cs index c4030fc3b..b4782415a 100644 --- a/src/Squirrel/UpdateManager.CheckForUpdates.cs +++ b/src/Squirrel/UpdateManager.CheckForUpdates.cs @@ -23,7 +23,7 @@ public CheckForUpdateImpl(string rootAppDirectory) public async Task CheckForUpdate( string localReleaseFile, string updateUrlOrPath, - bool ignoreDeltaUpdates = false, + bool ignoreDeltaUpdates = false, Action progress = null, IFileDownloader urlDownloader = null) { @@ -45,11 +45,11 @@ public async Task CheckForUpdate( string releaseFile; - var latestLocalRelease = localReleases.Count() > 0 ? - localReleases.MaxBy(x => x.Version).First() : + var latestLocalRelease = localReleases.Count() > 0 ? + localReleases.MaxBy(x => x.Version).First() : default(ReleaseEntry); - // Fetch the remote RELEASES file, whether it's a local dir or an + // Fetch the remote RELEASES file, whether it's a local dir or an // HTTP URL if (Utility.IsHttpUrl(updateUrlOrPath)) { if (updateUrlOrPath.EndsWith("/")) { @@ -89,7 +89,7 @@ public async Task CheckForUpdate( if (!Directory.Exists(updateUrlOrPath)) { var message = String.Format( - "The directory {0} does not exist, something is probably broken with your application", + "The directory {0} does not exist, something is probably broken with your application", updateUrlOrPath); throw new Exception(message); @@ -98,7 +98,7 @@ public async Task CheckForUpdate( var fi = new FileInfo(Path.Combine(updateUrlOrPath, "RELEASES")); if (!fi.Exists) { var message = String.Format( - "The file {0} does not exist, something is probably broken with your application", + "The file {0} does not exist, something is probably broken with your application", fi.FullName); this.Log().Warn(message); @@ -118,7 +118,7 @@ public async Task CheckForUpdate( } var ret = default(UpdateInfo); - var remoteReleases = ReleaseEntry.ParseReleaseFileAndApplyStaging(releaseFile, stagingId); + var remoteReleases = ReleaseEntry.ParseReleaseFileAndApplyStaging(releaseFile, stagingId); progress(66); if (!remoteReleases.Any()) { @@ -126,7 +126,7 @@ public async Task CheckForUpdate( } ret = determineUpdateInfo(localReleases, remoteReleases, ignoreDeltaUpdates); - + progress(100); return ret; } @@ -192,7 +192,7 @@ UpdateInfo determineUpdateInfo(IEnumerable localReleases, IEnumera this.Log().Info("Using existing staging user ID: {0}", ret.ToString()); return ret; } catch (Exception ex) { - this.Log().InfoException("Couldn't read staging user ID, creating a blank one", ex); + this.Log().DebugException("Couldn't read staging user ID, creating a blank one", ex); } var prng = new Random(); From 30e99c22557f4ac6c4002f27c3797aed5021ccaa Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Wed, 20 Apr 2016 10:44:54 -0700 Subject: [PATCH 15/16] :memo: for staged rollouts --- docs/readme.md | 1 + docs/using/staged-rollouts.md | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 docs/using/staged-rollouts.md diff --git a/docs/readme.md b/docs/readme.md index 02a0fadb0..8aa7d8a2c 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -50,6 +50,7 @@ The **[Getting Started Guide](getting-started/0-overview.md)** provides a step-b * [Update Manager](using/update-manager.md) - reference guide for the `UpdateManager`. * [GitHub](using/github.md) - overview of using GitHub for installing, distributing, and updating. * [Debugging Updates](using/debugging-updates.md) - tips for debugging Squirrel.Windows updates. + * [Staged Rollouts](using/staged-rollouts.md) - how to use staged rollouts to ramp up install distribution over time ## Contributing diff --git a/docs/using/staged-rollouts.md b/docs/using/staged-rollouts.md new file mode 100644 index 000000000..df02b9f90 --- /dev/null +++ b/docs/using/staged-rollouts.md @@ -0,0 +1,50 @@ +| [docs](..) / [using](.) / staged-rollouts.md +|:---| + +# Staged Rollouts + +Staged rollouts allow you to distribute the latest version of your app to a subset of users that you can increase over time, similar to rollouts on platforms like Google Play. This feature requires Squirrel.Windows 1.4.0 or above. + +### How to use + +Staged rollouts are controlled by manually editing your `RELEASES` file. Here's an example: + +~~~ +e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725 +~~~ + +Now let's ship a new version to 10% of our userbase. + +``` +e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725 +0d777ea94c612e8bf1ea7379164caefba6e24463 myapp-1.0.1-delta.nupkg 6030# 10% +85f4d657f8424dd437d1b33cc4511ea7ad86b1a7 myapp-1.0.1-full.nupkg 600752# 10% +``` + +Note that the syntax is `# nn%` - due to a bug in earlier versions of Squirrel.Windows, for now, you *must* put the `#` immediately following the file size, no spaces. Once all of your users have Squirrel 1.4.0 or higher, you can add a space after the `#` (similar to a comment). + +Assuming that this rollout is going well, at some point you can upload a new version of the releases file: + +``` +e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725 +0d777ea94c612e8bf1ea7379164caefba6e24463 myapp-1.0.1-delta.nupkg 6030# 50% +85f4d657f8424dd437d1b33cc4511ea7ad86b1a7 myapp-1.0.1-full.nupkg 600752# 50% +``` + +When you're confident that this release has gone successfully, you can remove the comment so that 100% of users get the file: + +``` +e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725 +0d777ea94c612e8bf1ea7379164caefba6e24463 myapp-1.0.1-delta.nupkg 6030 +85f4d657f8424dd437d1b33cc4511ea7ad86b1a7 myapp-1.0.1-full.nupkg 600752 +``` + +### Handling failed rollouts + +If you want to pull a staged release because it hasn't gone well, you should hand-edit the RELEASES file to completely remove the bad version: + +~~~ +e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725 +~~~ + +Once you do this, you **must** increment the version number higher than your broken release (in this example, we would need to release MyApp 1.0.2). Because some of your users will be on the broken 1.0.1, releasing a _new_ 1.0.1 would result in them staying on a broken version. From fe88624f412d128f4bc1cbbd00e6374c6d63975a Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Wed, 20 Apr 2016 10:46:32 -0700 Subject: [PATCH 16/16] Fix a thing --- docs/using/staged-rollouts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using/staged-rollouts.md b/docs/using/staged-rollouts.md index df02b9f90..f9dac01cd 100644 --- a/docs/using/staged-rollouts.md +++ b/docs/using/staged-rollouts.md @@ -23,7 +23,7 @@ e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725 Note that the syntax is `# nn%` - due to a bug in earlier versions of Squirrel.Windows, for now, you *must* put the `#` immediately following the file size, no spaces. Once all of your users have Squirrel 1.4.0 or higher, you can add a space after the `#` (similar to a comment). -Assuming that this rollout is going well, at some point you can upload a new version of the releases file: +Assuming that this rollout is going well, at some point you can upload a new version of the `RELEASES` file: ``` e3f67244e4166a65310c816221a12685c83f8e6f myapp-1.0.0-full.nupkg 600725