mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Split out new UpdateOptions class and add downgrade option
This commit is contained in:
@@ -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)) {
|
||||
|
||||
74
src/Velopack/UpdateManager.Helpers.cs
Normal file
74
src/Velopack/UpdateManager.Helpers.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
32
src/Velopack/UpdateOptions.cs
Normal file
32
src/Velopack/UpdateOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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 + ")");
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ try {
|
||||
return -1;
|
||||
}
|
||||
Console.WriteLine("applying...");
|
||||
um.ApplyUpdatesAndRestart(new[] { "test", "args !!" });
|
||||
um.ApplyUpdatesAndRestart((VelopackAsset) null, new[] { "test", "args !!" });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user