Split out new UpdateOptions class and add downgrade option

This commit is contained in:
Caelan Sayler
2024-01-31 11:55:55 +00:00
parent 091f6dcb92
commit 7366f7767d
7 changed files with 178 additions and 71 deletions

View File

@@ -13,7 +13,7 @@ namespace Velopack
/// <summary>
/// A static helper class to assist in running Update.exe CLI commands. You probably should not invoke this directly,
/// instead you should use the relevant methods on <see cref="UpdateManager"/>. For example:
/// <see cref="UpdateManager.ApplyUpdatesAndExit()"/>, or <see cref="UpdateManager.ApplyUpdatesAndRestart(string[])"/>.
/// <see cref="UpdateManager.ApplyUpdatesAndExit(VelopackAsset)"/>, or <see cref="UpdateManager.ApplyUpdatesAndRestart(VelopackAsset, string[])"/>.
/// </summary>
public static class UpdateExe
{
@@ -29,12 +29,15 @@ namespace Velopack
/// a new framework dependency.</param>
/// <param name="restart">If true, restarts the application after updates are applied (or if they failed)</param>
/// <param name="locator">The locator to use to find the path to Update.exe and the packages directory.</param>
/// <param name="toApply">The update package you wish to apply, can be left null.</param>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
/// <param name="logger">The logger to use for diagnostic messages</param>
/// <exception cref="Exception">Thrown if Update.exe does not initialize properly.</exception>
public static void Apply(IVelopackLocator locator, bool silent, bool restart, string[]? restartArgs = null, ILogger? logger = null)
public static void Apply(IVelopackLocator? locator, VelopackAsset? toApply, bool silent, bool restart, string[]? restartArgs = null, ILogger? logger = null)
{
logger ??= NullLogger.Instance;
locator ??= VelopackLocator.GetDefault(logger);
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = locator.UpdateExePath,
@@ -46,7 +49,7 @@ namespace Velopack
args.Add("apply");
args.Add("--wait");
var entry = locator.GetLatestLocalFullPackage();
var entry = toApply ?? locator.GetLatestLocalFullPackage();
if (entry != null && locator.PackagesDir != null) {
var pkg = Path.Combine(locator.PackagesDir, entry.FileName);
if (File.Exists(pkg)) {

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Velopack
{
public partial class UpdateManager
{
/// <inheritdoc cref="ApplyUpdatesAndRestart(VelopackAsset, string[])"/>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Please use one of the other overloads of ApplyUpdatesAndRestart() instead.")]
public void ApplyUpdatesAndRestart(string[]? restartArgs = null)
=> ApplyUpdatesAndRestart((VelopackAsset?) null, restartArgs);
/// <inheritdoc cref="ApplyUpdatesAndRestart(VelopackAsset, string[])"/>
public void ApplyUpdatesAndRestart(UpdateInfo? toApply, string[]? restartArgs = null)
=> ApplyUpdatesAndRestart(toApply?.TargetFullRelease, restartArgs);
/// <summary>
/// This will exit your app immediately, apply updates, and then optionally relaunch the app using the specified
/// restart arguments. If you need to save state or clean up, you should do that before calling this method.
/// The user may be prompted during the update, if the update requires additional frameworks to be installed etc.
/// You can check if there are pending updates by checking <see cref="IsUpdatePendingRestart"/>.
/// </summary>
/// <param name="toApply">The target release to apply. Can be left null to auto-apply the newest downloaded release.</param>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
public void ApplyUpdatesAndRestart(VelopackAsset? toApply, string[]? restartArgs = null)
{
WaitExitThenApplyUpdates(toApply, silent: false, restart: true, restartArgs);
Environment.Exit(0);
}
/// <inheritdoc cref="ApplyUpdatesAndExit(VelopackAsset)"/>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Please use one of the other overloads of ApplyUpdatesAndExit() instead.")]
public void ApplyUpdatesAndExit(string[]? restartArgs = null)
=> ApplyUpdatesAndExit((VelopackAsset?) null);
/// <inheritdoc cref="ApplyUpdatesAndExit(VelopackAsset)"/>
public void ApplyUpdatesAndExit(UpdateInfo? toApply)
=> ApplyUpdatesAndExit(toApply?.TargetFullRelease);
/// <summary>
/// This will exit your app immediately, apply updates, and then optionally relaunch the app using the specified
/// restart arguments. If you need to save state or clean up, you should do that before calling this method.
/// The user may be prompted during the update, if the update requires additional frameworks to be installed etc.
/// You can check if there are pending updates by checking <see cref="IsUpdatePendingRestart"/>.
/// </summary>
/// <param name="toApply">The target release to apply. Can be left null to auto-apply the newest downloaded release.</param>
public void ApplyUpdatesAndExit(VelopackAsset? toApply)
{
WaitExitThenApplyUpdates(toApply, silent: true, restart: false);
Environment.Exit(0);
}
/// <summary>
/// This will launch the Velopack updater and tell it to wait for this program to exit gracefully.
/// You should then clean up any state and exit your app. The updater will apply updates and then
/// optionally restart your app. The updater will only wait for 60 seconds before giving up.
/// You can check if there are pending updates by checking <see cref="IsUpdatePendingRestart"/>.
/// </summary>
/// <param name="toApply">The target release to apply. Can be left null to auto-apply the newest downloaded release.</param>
/// <param name="silent">Configure whether Velopack should show a progress window / dialogs during the updates or not.</param>
/// <param name="restart">Configure whether Velopack should restart the app after the updates have been applied.</param>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
public void WaitExitThenApplyUpdates(VelopackAsset? toApply, bool silent = false, bool restart = true, string[]? restartArgs = null)
{
UpdateExe.Apply(Locator, toApply, silent, restart, restartArgs, Log);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -19,7 +19,7 @@ namespace Velopack
/// <summary>
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
/// </summary>
public class UpdateManager
public partial class UpdateManager
{
/// <summary> The currently installed application Id. This would be what you set when you create your release.</summary>
public virtual string? AppId => Locator.AppId;
@@ -27,7 +27,7 @@ namespace Velopack
/// <summary> True if this application is currently installed, and is able to download/check for updates. </summary>
public virtual bool IsInstalled => Locator.CurrentlyInstalledVersion != null;
/// <summary> True if there is a local update prepared that requires a call to <see cref="ApplyUpdatesAndRestart(string[])"/> to be applied. </summary>
/// <summary> True if there is a local update prepared that requires a call to <see cref="ApplyUpdatesAndRestart(VelopackAsset, string[])"/> to be applied. </summary>
public virtual bool IsUpdatePendingRestart {
get {
var latestLocal = Locator.GetLatestLocalFullPackage();
@@ -52,17 +52,26 @@ namespace Velopack
/// <summary> The channel to use when searching for packages. </summary>
protected string Channel { get; }
/// <summary> The default channel to search for packages in, if one was not provided by the user. </summary>
protected string DefaultChannel => Locator?.Channel ?? VelopackRuntimeInfo.SystemOs.GetOsShortName();
/// <summary> If true, an explicit channel was provided by the user, and it's different than the default channel. </summary>
protected bool IsNonDefaultChannel => Locator?.Channel != null && Channel != DefaultChannel;
/// <summary> If true, UpdateManager should return the latest asset in the feed, even if that version is lower than the current version. </summary>
protected bool ShouldAllowVersionDowngrade { get; }
/// <summary>
/// Creates a new UpdateManager instance using the specified URL or file path to the releases feed, and the specified channel name.
/// </summary>
/// <param name="urlOrPath">A basic URL or file path to use when checking for updates.</param>
/// <param name="channel">Search for releases in the feed of a specific channel name. If null, it will search the default channel.</param>
/// <param name="options">Override / configure default update behaviors.</param>
/// <param name="logger">The logger to use for diagnostic messages. If one was provided to <see cref="VelopackApp.Run(ILogger)"/> but is null here,
/// it will be cached and used again.</param>
/// <param name="locator">This should usually be left null. Providing an <see cref="IVelopackLocator" /> allows you to mock up certain application paths.
/// For example, if you wanted to test that updates are working in a unit test, you could provide an instance of <see cref="TestVelopackLocator"/>. </param>
public UpdateManager(string urlOrPath, string? channel = null, ILogger? logger = null, IVelopackLocator? locator = null)
: this(CreateSimpleSource(urlOrPath), channel, logger, locator)
public UpdateManager(string urlOrPath, UpdateOptions? options = null, ILogger? logger = null, IVelopackLocator? locator = null)
: this(CreateSimpleSource(urlOrPath), options, logger, locator)
{
}
@@ -71,12 +80,12 @@ namespace Velopack
/// </summary>
/// <param name="source">The source describing where to search for updates. This can be a custom source, if you are integrating with some private resource,
/// or it could be one of the predefined sources. (eg. <see cref="SimpleWebSource"/> or <see cref="GithubSource"/>, etc).</param>
/// <param name="channel">Search for releases in the feed of a specific channel name. If null, it will search the default channel.</param>
/// <param name="options">Override / configure default update behaviors.</param>
/// <param name="logger">The logger to use for diagnostic messages. If one was provided to <see cref="VelopackApp.Run(ILogger)"/> but is null here,
/// it will be cached and used again.</param>
/// <param name="locator">This should usually be left null. Providing an <see cref="IVelopackLocator" /> allows you to mock up certain application paths.
/// For example, if you wanted to test that updates are working in a unit test, you could provide an instance of <see cref="TestVelopackLocator"/>. </param>
public UpdateManager(IUpdateSource source, string? channel = null, ILogger? logger = null, IVelopackLocator? locator = null)
public UpdateManager(IUpdateSource source, UpdateOptions? options = null, ILogger? logger = null, IVelopackLocator? locator = null)
{
if (source == null) {
throw new ArgumentNullException(nameof(source));
@@ -84,7 +93,8 @@ namespace Velopack
Source = source;
Log = logger ?? VelopackApp.DefaultLogger ?? NullLogger.Instance;
Locator = locator ?? VelopackLocator.GetDefault(Log);
Channel = channel ?? Locator.Channel ?? VelopackRuntimeInfo.SystemOs.GetOsShortName();
Channel = options?.ExplicitChannel ?? DefaultChannel;
ShouldAllowVersionDowngrade = options?.AllowVersionDowngrade ?? false;
}
/// <inheritdoc cref="CheckForUpdatesAsync()"/>
@@ -116,32 +126,56 @@ namespace Velopack
return null;
}
if (latestRemoteFull.Version <= installedVer) {
Log.Info($"No updates, remote version ({latestRemoteFull.Version}) is not newer than current version ({installedVer}).");
return null;
// there's a newer version available, easy.
if (latestRemoteFull.Version > installedVer) {
Log.Info($"Found newer remote release available ({installedVer} -> {latestRemoteFull.Version}).");
return CreateDeltaUpdateStrategy(feed, latestLocalFull, latestRemoteFull);
}
Log.Info($"Found remote update available ({latestRemoteFull.Version}).");
// if the remote version is < than current version and downgrade is enabled
if (latestRemoteFull.Version < installedVer && ShouldAllowVersionDowngrade) {
Log.Info($"Latest remote release is older than current, and downgrade is enabled ({installedVer} -> {latestRemoteFull.Version}).");
return new UpdateInfo(latestRemoteFull);
}
// if the remote version is the same as current version, and downgrade is enabled,
// and we're searching for a different channel than current
if (ShouldAllowVersionDowngrade && IsNonDefaultChannel) {
if (VersionComparer.Compare(latestRemoteFull.Version, installedVer, VersionComparison.Version) == 0) {
Log.Info($"Latest remote release is the same version of a different channel, and downgrade is enabled ({installedVer}: {DefaultChannel} -> {Channel}).");
return new UpdateInfo(latestRemoteFull);
}
}
Log.Info($"No updates, remote version ({latestRemoteFull.Version}) is not newer than current version ({installedVer}) and / or downgrade is not enabled.");
return null;
}
/// <summary>
/// Given a feed of releases, and the latest local full release, and the latest remote full release, this method will return a delta
/// update strategy to be used by <see cref="DownloadUpdatesAsync(UpdateInfo, Action{int}?, bool, CancellationToken)"/>.
/// </summary>
protected virtual UpdateInfo CreateDeltaUpdateStrategy(VelopackAsset[] feed, VelopackAsset? latestLocalFull, VelopackAsset latestRemoteFull)
{
if (latestLocalFull == null) {
// TODO: for now, we're not trying to handle the case of building delta updates on top of an installation directory,
// but we can look at this in the future. Until then, Windows (installer) is the only thing which ships with a complete .nupkg
// so in all other cases, Velopack needs to download one full release before it can start using delta's.
Log.Info("There is no local/base package available for this update, so delta updates will be disabled.");
return new UpdateInfo(latestRemoteFull);
}
EnsureInstalled();
var installedVer = CurrentVersion!;
var matchingRemoteDelta = feed.Where(r => r.Type == VelopackAssetType.Delta && r.Version == latestRemoteFull.Version).FirstOrDefault();
if (matchingRemoteDelta == null) {
Log.Info($"Unable to find delta matching version {latestRemoteFull.Version}, only full update will be available.");
Log.Info($"Unable to find any delta matching version {latestRemoteFull.Version}, so delta updates will be disabled.");
return new UpdateInfo(latestRemoteFull);
}
// if we have a local full release, we try to apply delta's from that version to target version.
// if we do not have a local release, we try to apply delta's to a copy of the current installed app files.
SemanticVersion? deltaFromVer = null;
if (latestLocalFull != null) {
if (latestLocalFull.Version != installedVer) {
Log.Warn($"The current running version is {installedVer}, however the latest available local full .nupkg " +
$"is {latestLocalFull.Version}. We will try to download and apply delta's from {latestLocalFull.Version}.");
}
deltaFromVer = latestLocalFull.Version;
} else {
Log.Warn("There is no local release .nupkg, we are going to attempt an in-place delta upgrade using application files.");
deltaFromVer = installedVer;
}
SemanticVersion deltaFromVer = latestLocalFull.Version;
var deltas = feed.Where(r => r.Type == VelopackAssetType.Delta && r.Version > deltaFromVer && r.Version <= latestRemoteFull.Version).ToArray();
Log.Debug($"Found {deltas.Length} delta releases between {deltaFromVer} and {latestRemoteFull.Version}.");
@@ -286,44 +320,6 @@ namespace Velopack
}
}
/// <summary>
/// This will exit your app immediately, apply updates, and then optionally relaunch the app using the specified
/// restart arguments. If you need to save state or clean up, you should do that before calling this method.
/// The user may be prompted during the update, if the update requires additional frameworks to be installed etc.
/// You can check if there are pending updates by checking <see cref="IsUpdatePendingRestart"/>.
/// </summary>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
public void ApplyUpdatesAndRestart(string[]? restartArgs = null)
{
WaitExitThenApplyUpdates(true, restartArgs);
Environment.Exit(0);
}
/// <summary>
/// This will exit your app immediately, apply updates, and then optionally relaunch the app using the specified
/// restart arguments. If you need to save state or clean up, you should do that before calling this method.
/// The user may be prompted during the update, if the update requires additional frameworks to be installed etc.
/// You can check if there are pending updates by checking <see cref="IsUpdatePendingRestart"/>.
/// </summary>
public void ApplyUpdatesAndExit()
{
WaitExitThenApplyUpdates(false, null);
Environment.Exit(0);
}
/// <summary>
/// This will launch the Velopack updater and tell it to wait for this program to exit gracefully.
/// You should then clean up any state and exit your app. The updater will apply updates and then
/// optionally restart your app. The updater will only wait for 60 seconds before giving up.
/// You can check if there are pending updates by checking <see cref="IsUpdatePendingRestart"/>.
/// </summary>
/// <param name="restart">Whether Velopack should restart the app after the updates have been applied.</param>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
public void WaitExitThenApplyUpdates(bool restart = true, string[]? restartArgs = null)
{
UpdateExe.Apply(Locator, false, restart, restartArgs, Log);
}
/// <summary>
/// Given a folder containing the extracted base package, and a list of delta updates, downloads and applies the
/// delta updates to the base package.

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Velopack
{
/// <summary>
/// Options to customise the behaviour of <see cref="UpdateManager"/>.
/// </summary>
public class UpdateOptions
{
/// <summary>
/// Allows UpdateManager to update to a version that's lower than the current version (i.e. downgrading).
/// This could happen if a release has bugs and was retracted from the release feed, or if you're using
/// <see cref="ExplicitChannel"/> to switch channels to another channel where the latest version on that
/// channel is lower than the current version.
/// </summary>
public bool AllowVersionDowngrade { get; set; }
/// <summary>
/// <b>This option should usually be left null</b>. Overrides the default channel used to fetch updates.
/// The default channel will be whatever channel was specified on the command line when building this release.
/// For example, if the current release was packaged with '--channel beta', then the default channel will be 'beta'.
/// This allows users to automatically receive updates from the same channel they installed from. This options
/// allows you to explicitly switch channels, for example if the user wished to switch back to the 'stable' channel
/// without having to reinstall the application.
/// </summary>
public string? ExplicitChannel { get; set; }
}
}

View File

@@ -213,7 +213,7 @@ namespace Velopack
log.Info($"Launching app is out-dated. Current: {myVersion}, Newest Local Available: {latestLocal.Version}");
if (!restarted && _autoApply) {
log.Info("Auto apply is true, so restarting to apply update...");
UpdateExe.Apply(locator, true, true, args, log);
UpdateExe.Apply(locator, latestLocal, true, true, args, log);
} else {
log.Info("Pre-condition failed, we will not restart to apply updates. (restarted: " + restarted + ", autoApply: " + _autoApply + ")");
}

View File

@@ -78,7 +78,7 @@ try {
return -1;
}
Console.WriteLine("applying...");
um.ApplyUpdatesAndRestart(new[] { "test", "args !!" });
um.ApplyUpdatesAndRestart((VelopackAsset) null, new[] { "test", "args !!" });
return 0;
}
}

View File

@@ -124,7 +124,8 @@ namespace Velopack.Tests
var dl = GetMockDownloaderNoDelta();
var source = new SimpleWebSource("http://any.com", dl);
var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger);
var um = new UpdateManager(source, "experimental", logger, locator);
var opt = new UpdateOptions { ExplicitChannel = "experimental" };
var um = new UpdateManager(source, opt, logger, locator);
var info = um.CheckForUpdates();
Assert.NotNull(info);
Assert.True(new SemanticVersion(1, 1, 0) == info.TargetFullRelease.Version);
@@ -170,7 +171,8 @@ namespace Velopack.Tests
using var _1 = Utility.GetTempDirectory(out var tempPath);
var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger);
var source = new GithubSource("https://github.com/caesay/SquirrelCustomLauncherTestApp", null, false);
var um = new UpdateManager(source, "hello", logger, locator);
var opt = new UpdateOptions { ExplicitChannel = "hello" };
var um = new UpdateManager(source, opt, logger, locator);
Assert.Throws<ArgumentException>(() => um.CheckForUpdates());
}