Skip to content

Commit

Permalink
First shot at implementing an appcast filtering process that allows r…
Browse files Browse the repository at this point in the history
…egression to a lower version number.
  • Loading branch information
johncclayton committed Feb 12, 2022
1 parent 63f4640 commit db0a7ff
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 54 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ src/NetSparkle.Tools.AppCastGenerator/NetSparkle.Tools.AppCastGenerator.csproj.u
doc/generated/*
doc/Working Data
!git.folder

/.idea
UpgradeLog.htm
129 changes: 82 additions & 47 deletions src/NetSparkle/AppCastHandlers/XMLAppCast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,66 +182,101 @@ protected virtual void ParseAppCast(string appCast)
}

/// <summary>
/// Returns sorted list of updates between current installed version and latest version in <see cref="Items"/>.
/// Currently installed version is NOT included in the output.
/// Check if an AppCastItem update is valid, according to platform, signature requirements and current installed version number.
/// In the case where your app implements a downgrade strategy, e.g. when switching from a beta to a
/// stable channel - there has to be a way to tell the update mechanism that you wish to ignore
/// the beta AppCastItem elements, and that the latest stable element should be installed.
/// </summary>
/// <returns>A list of <seealso cref="AppCastItem"/> updates that could be installed</returns>
public virtual List<AppCastItem> GetAvailableUpdates()
/// <param name="installed">the currently installed Version</param>
/// <param name="signatureNeeded">whether or not a signature is required</param>
/// <param name="item">the AppCastItem under consideration, every AppCastItem found in the appcast.xml file is presented to this function once</param>
/// <returns>MatchingResult.MatchOk if the AppCastItem should be considered as a valid target for installation.</returns>
public MatchingResult IsMatchingUpdate(Version installed, bool signatureNeeded, AppCastItem item)
{
Version installed = new Version(_config.InstalledVersion);
var signatureNeeded = Utilities.IsSignatureNeeded(_signatureVerifier.SecurityMode, _signatureVerifier.HasValidKeyInformation(), false);
_logWriter.PrintMessage("Looking for available updates; our installed version is {0}; do we need a signature? {1}", installed, signatureNeeded);
return Items.Where((item) =>
{
#if NETFRAMEWORK
// don't allow non-windows updates
if (!item.IsWindowsUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a Windows update and we're on Windows", item.Version,
item.ShortVersion, item.Title);
return false;
return MatchingResult.NotThisPlatform;
}
#else
// check operating system and filter out ones that don't match the current
// operating system
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !item.IsWindowsUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a Windows update and we're on Windows", item.Version,
item.ShortVersion, item.Title);
return false;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !item.IsMacOSUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a macOS update and we're on macOS", item.Version,
item.ShortVersion, item.Title);
return false;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !item.IsLinuxUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a Linux update and we're on Linux", item.Version,
item.ShortVersion, item.Title);
return false;
}
// check operating system and filter out ones that don't match the current
// operating system
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !item.IsWindowsUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a Windows update and we're on Windows", item.Version,
item.ShortVersion, item.Title);
return MatchingResult.NotThisPlatform;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !item.IsMacOSUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a macOS update and we're on macOS", item.Version,
item.ShortVersion, item.Title);
return MatchingResult.NotThisPlatform;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !item.IsLinuxUpdate)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it isn't a Linux update and we're on Linux", item.Version,
item.ShortVersion, item.Title);
return MatchingResult.NotThisPlatform;
}
#endif
// filter smaller versions
if (new Version(item.Version).CompareTo(installed) <= 0)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it is older than our current version of {3}", item.Version,
item.ShortVersion, item.Title, installed);
return false;
}
// filter versions without signature if we need signatures. But accept version without downloads.
if (signatureNeeded && string.IsNullOrEmpty(item.DownloadSignature) && !string.IsNullOrEmpty(item.DownloadLink))
// filter smaller versions
if (new Version(item.Version).CompareTo(installed) <= 0)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it is older than our current version of {3}", item.Version,
item.ShortVersion, item.Title, installed);
return MatchingResult.VersionIsOlderThanCurrent;
}
// filter versions without signature if we need signatures. But accept version without downloads.
if (signatureNeeded && string.IsNullOrEmpty(item.DownloadSignature) && !string.IsNullOrEmpty(item.DownloadLink))
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it we needed a DSA/other signature and " +
"the item has no signature yet has a download link of {3}", item.Version,
item.ShortVersion, item.Title, item.DownloadLink);
return MatchingResult.SignatureIsMissing;
}

return MatchingResult.MatchOk;
}

/// <summary>
/// Returns sorted list of updates between current installed version and latest version in <see cref="Items"/>.
/// Currently installed version is NOT included in the output.
/// </summary>
/// <returns>A list of <seealso cref="AppCastItem"/> updates that could be installed</returns>
public virtual List<AppCastItem> GetAvailableUpdates(IAppCastFilter customFilter = null)
{
Version installed = new Version(_config.InstalledVersion);
List<AppCastItem> appCastItems = Items;

if (customFilter != null)
{
Tuple<Version, List<AppCastItem>> result = customFilter.GetFilteredAppCastItems(installed, Items);

// force a low version number, this in turn means the rest of the code will just pick the highest one
// TODO: find a way to decrement System.Version by 1 properly; so that the releases notes window only shows the last update.
installed = result.Item1;
appCastItems = result.Item2;
}

var signatureNeeded = Utilities.IsSignatureNeeded(_signatureVerifier.SecurityMode, _signatureVerifier.HasValidKeyInformation(), false);

_logWriter.PrintMessage("Looking for available updates; our installed version is {0}; do we need a signature? {1}", installed, signatureNeeded);
return appCastItems.Where((item) =>
{
if(IsMatchingUpdate(installed, signatureNeeded, item) == MatchingResult.MatchOk)
{
_logWriter.PrintMessage("Rejecting update for {0} ({1}, {2}) because it we needed a DSA/other signature and " +
"the item has no signature yet has a download link of {3}", item.Version,
item.ShortVersion, item.Title, item.DownloadLink);
return false;
// accept everything else
_logWriter.PrintMessage("Item with version {0} ({1}) is a valid update! It can be downloaded at {2}", item.Version,
item.ShortVersion, item.DownloadLink);

return true;
}
// accept everything else
_logWriter.PrintMessage("Item with version {0} ({1}) is a valid update! It can be downloaded at {2}", item.Version,
item.ShortVersion, item.DownloadLink);
return true;

return false;
}).ToList();
}

Expand Down
39 changes: 39 additions & 0 deletions src/NetSparkle/Interfaces/IAppCastFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace NetSparkleUpdater.Interfaces
{
/// <summary>
/// This provides a way to filter out AppCast item instances that do not apply to the current update process. Originally
/// used to make it possible to revert from a "beta" version to a "stable" one - where the current runtime System.Version value
/// is by definition higher than anything in the "stable" app cast list.
///
/// To force NetSparkle to upgrade to a lower version number, return a System.Version("0.0.0.0") and a list of app cast items that are valid candidates to update - the
/// GetFilteredAppCastItems() method allows you to remove elements from this list before the standard NetSparkle code does its thing.
///
/// </summary>
public interface IAppCastFilter
{
/// <summary>
/// Returns a new "minimum" version number and a potentially modified list of app cast items, e.g. maybe you want to remove the beta ones when reverting to stable.
/// </summary>
/**
<example>
This code shows how to do no filtering at all.
<code>
if(noFilteringRequired)
{
return new Tuple&lt;installed, List&lt;AppCastItem&gt;&gt;(installed, items);
}
</code>
</example>
*/
/// <remarks>The app must return a version and list of app cast items. If there is no need to filter then simply return the values that were passed in.</remarks>
/// <remarks>Note: calls to methods on this interface are made from background threads - dont access UI objects from within this method</remarks>
/// <param name="installed">The currently detected version of this application</param>
/// <param name="items">The current set of AppCastItem objects</param>
/// <returns>A replacement list of items derived from the input set</returns>
Tuple<Version, List<AppCastItem>> GetFilteredAppCastItems(Version installed, List<AppCastItem> items);
}
}
18 changes: 16 additions & 2 deletions src/NetSparkle/Interfaces/IAppCastHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NetSparkleUpdater.Configurations;
using NetSparkleUpdater.AppCastHandlers;
using NetSparkleUpdater.Configurations;
using System;
using System.Collections.Generic;
using System.Text;
Expand Down Expand Up @@ -45,7 +46,20 @@ void SetupAppCastHandler(IAppCastDataDownloader dataDownloader, string castUrl,
/// This should be called after <see cref="DownloadAndParse"/> has
/// successfully completed.
/// </summary>
/// <param name="customFilter">A filter interface used to influence what will be included in the set of <see cref="AppCastItem"/>s</param>
/// <returns>a list of <see cref="AppCastItem"/> updates. Can be empty if no updates are available.</returns>
List<AppCastItem> GetAvailableUpdates();
List<AppCastItem> GetAvailableUpdates(IAppCastFilter customFilter = null);

/// <summary>
/// Check if an <see cref="AppCastItem"/> update is valid, according to platform, signature requirements and current installed version number.
/// In the case where your app implements a downgrade strategy, e.g. when switching from a beta to a
/// stable channel - there has to be a way to tell the update mechanism that you wish to ignore
/// the beta AppCastItem elements, and that the latest stable element should be installed.
/// </summary>
/// <param name="installed">the currently installed Version</param>
/// <param name="signatureNeeded">whether or not a signature is required</param>
/// <param name="item">the AppCastItem under consideration, every AppCastItem found in the appcast.xml file is presented to this function once</param>
/// <returns>MatchingResult.MatchOk if the AppCastItem should be considered as a valid target for installation.</returns>
MatchingResult IsMatchingUpdate(Version installed, bool signatureNeeded, AppCastItem item);
}
}
27 changes: 27 additions & 0 deletions src/NetSparkle/Interfaces/MatchingResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace NetSparkleUpdater.Interfaces
{
/// <summary>
/// Provides the return values for the GetAvailableUpdates call on the IAppCastHandler. When an appcast is downloaded,
/// the IAppCastHandler will work out which AppCastItem instances match criteria for an update.
/// </summary>
/// <seealso cref="IAppCastHandler.GetAvailableUpdates"/>
public enum MatchingResult
{
/// <summary>
/// Indicates that the AppCastItem is a validate candidate for installation.
/// </summary>
MatchOk = 0,
/// <summary>
/// The AppCastItem is for a different operating system that this one, and must be ignored.
/// </summary>
NotThisPlatform = 1,
/// <summary>
/// The version indicated by the AppCastItem is less than or equal to the currently installed/running version.
/// </summary>
VersionIsOlderThanCurrent = 2,
/// <summary>
/// A signature is required to validate the item - but it's missing from the AppCastItem
/// </summary>
SignatureIsMissing = 3
}
}
2 changes: 1 addition & 1 deletion src/NetSparkle/NetSparkle.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>netstandard2.0;net452</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>NetSparkleUpdater.SparkleUpdater</PackageId>
<Version>2.0.9</Version>
<Version>2.0.9-preview202202122103</Version>
<Authors>Deadpikle, Dirk Eisenberg</Authors>
<Description>NetSparkleUpdater/NetSparkle is a C# .NET software update framework that allows you to easily download installer files and update your C# .NET Framework or .NET Core software. Built-in UIs are available for WinForms, WPF, and Avalonia; if you want a built-in UI, please reference a NetSparkleUpdater.UI package. You provide, somewhere on the internet, an XML appcast with software version information along with release notes in Markdown or HTML format. The NetSparkle framework then checks for an update in the background, displays the release notes to the user, and lets users download or skip the software update. The framework can also perform silent downloads so that you can present all of the UI yourself or set up your own silent software update system, as allowed by your software architecture. It was inspired by the Sparkle (https://sparkle-project.org/) project for Cocoa developers and the WinSparkle (https://winsparkle.org/) project (a Win32 port).</Description>
<Copyright>Copyright 2010 - 2022</Copyright>
Expand Down
18 changes: 14 additions & 4 deletions src/NetSparkle/SparkleUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public partial class SparkleUpdater : IDisposable
private string _restartExecutablePath;

private IAppCastHandler _appCastHandler;
private IAppCastFilter _appCastFilter;

#endregion

Expand All @@ -86,7 +87,7 @@ public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier)
/// <param name="signatureVerifier">the object that will verify your app cast signatures.</param>
/// <param name="referenceAssembly">the name of the assembly to use for comparison when checking update versions</param>
public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly)
: this(appcastUrl, signatureVerifier, referenceAssembly, null)
: this(appcastUrl, signatureVerifier, referenceAssembly, null, null)
{ }

/// <summary>
Expand All @@ -96,10 +97,12 @@ public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, s
/// <param name="signatureVerifier">the object that will verify your app cast signatures.</param>
/// <param name="referenceAssembly">the name of the assembly to use for comparison when checking update versions</param>
/// <param name="factory">a UI factory to use in place of the default UI</param>
public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly, IUIFactory factory)
/// <param name="matcher">The optional matching delegate that can be used to filter appcastitem elements in/out of the upgrade process</param>
public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly, IUIFactory factory, IAppCastFilter matcher)
{
_latestDownloadedUpdateInfo = null;
_hasAttemptedFileRedownload = false;
AppCastFilter = matcher;
UIFactory = factory;
SignatureVerifier = signatureVerifier;
// Syncronization Context
Expand Down Expand Up @@ -428,7 +431,14 @@ public IAppCastHandler AppCastHandler
set => _appCastHandler = value;
}

#endregion
/// <summary>
/// The custom appcast filter instance - can also be null; usually used to implement a downgrade strategy
/// <seealso cref="IAppCastFilter"/>
/// </summary>
/// <remarks>Note: calls to methods on this interface are made from background threads - dont access UI objects from within this method</remarks>
public IAppCastFilter AppCastFilter { get => _appCastFilter; set => _appCastFilter = value; }

#endregion

/// <summary>
/// Starts a SparkleUpdater background loop to check for updates every 24 hours.
Expand Down Expand Up @@ -618,7 +628,7 @@ await Task.Factory.StartNew(() =>
if (AppCastHandler.DownloadAndParse())
{
LogWriter.PrintMessage("App cast successfully downloaded and parsed. Getting available updates...");
updates = AppCastHandler.GetAvailableUpdates();
updates = AppCastHandler.GetAvailableUpdates(AppCastFilter);
}
});
}
Expand Down

0 comments on commit db0a7ff

Please sign in to comment.