Add more specific exception types for lockfile and notinstalled

This commit is contained in:
Caelan Sayler
2025-01-10 11:37:20 +00:00
committed by Caelan
parent 143cbb052b
commit 379e533c78
8 changed files with 143 additions and 67 deletions

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Velopack.Exceptions;
using Velopack.Util;
namespace Velopack.Compression
@@ -62,31 +63,34 @@ namespace Velopack.Compression
new DirectoryInfo(workingPath).GetAllFilesRecursively()
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
.ForEach(x => {
Log.Trace($"{x} was in old package but not in new one, deleting");
File.Delete(Path.Combine(workingPath, x));
});
.ForEach(
x => {
Log.Trace($"{x} was in old package but not in new one, deleting");
File.Delete(Path.Combine(workingPath, x));
});
progress(85);
// Add all of the files that are in the new package but
// not in the old one.
deltaPathRelativePaths
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)
&& !x.EndsWith(".shasum", StringComparison.InvariantCultureIgnoreCase)
&& !pathsVisited.Contains(DIFF_SUFFIX.Replace(x, ""), StringComparer.InvariantCultureIgnoreCase))
.ForEach(x => {
Log.Trace($"{x} was in new package but not in old one, adding");
.Where(
x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)
&& !x.EndsWith(".shasum", StringComparison.InvariantCultureIgnoreCase)
&& !pathsVisited.Contains(DIFF_SUFFIX.Replace(x, ""), StringComparer.InvariantCultureIgnoreCase))
.ForEach(
x => {
Log.Trace($"{x} was in new package but not in old one, adding");
string outputFile = Path.Combine(workingPath, x);
string outputDirectory = Path.GetDirectoryName(outputFile)!;
string outputFile = Path.Combine(workingPath, x);
string outputDirectory = Path.GetDirectoryName(outputFile)!;
if (!Directory.Exists(outputDirectory)) {
Directory.CreateDirectory(outputDirectory);
}
if (!Directory.Exists(outputDirectory)) {
Directory.CreateDirectory(outputDirectory);
}
File.Copy(Path.Combine(deltaPath, x), outputFile);
});
File.Copy(Path.Combine(deltaPath, x), outputFile);
});
progress(95);
@@ -94,20 +98,23 @@ namespace Velopack.Compression
// package's versions (i.e. the nuspec file, etc etc).
deltaPathRelativePaths
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.ForEach(x => {
Log.Trace($"Writing metadata file: {x}");
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
});
.ForEach(
x => {
Log.Trace($"Writing metadata file: {x}");
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
});
// delete all metadata files that are not in the new package
new DirectoryInfo(workingPath).GetAllFilesRecursively()
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)
&& !deltaPathRelativePaths.Contains(x, StringComparer.InvariantCultureIgnoreCase))
.ForEach(x => {
Log.Trace($"Deleting removed metadata file: {x}");
File.Delete(Path.Combine(workingPath, x));
});
.Where(
x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)
&& !deltaPathRelativePaths.Contains(x, StringComparer.InvariantCultureIgnoreCase))
.ForEach(
x => {
Log.Trace($"Deleting removed metadata file: {x}");
File.Delete(Path.Combine(workingPath, x));
});
progress(100);
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Velopack.Exceptions
{
/// <summary>
/// Thrown when an exclusive lock for an application cannot be acquired. Usually this means another
/// instance of the application is running Velopack operations and the current instance cannot proceed.
/// </summary>
[ExcludeFromCodeCoverage]
public class AcquireLockFailedException : Exception
{
private const string DEFAULT_MESSAGE = "Failed to acquire exclusive lock file. Is another operation currently running?";
internal AcquireLockFailedException() : base(DEFAULT_MESSAGE) { }
internal AcquireLockFailedException(Exception innerException) : base(DEFAULT_MESSAGE, innerException) { }
internal AcquireLockFailedException(string message) : base(message) { }
internal AcquireLockFailedException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Velopack.Compression
namespace Velopack.Exceptions
{
/// <summary>
/// Represents an error that occurs when a package does not match it's expected SHA checksum
@@ -27,4 +27,4 @@ namespace Velopack.Compression
FilePath = filePath;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Velopack.Exceptions
{
/// <summary>
/// Thrown when an operation can not be performed in an application that is not installed.
/// </summary>
[ExcludeFromCodeCoverage]
public class NotInstalledException : Exception
{
private const string DEFAULT_MESSAGE =
"This operation can not be performed in an application that is not installed. Please install the application and try again.";
internal NotInstalledException() : base(DEFAULT_MESSAGE) { }
internal NotInstalledException(Exception innerException) : base(DEFAULT_MESSAGE, innerException) { }
internal NotInstalledException(string message) : base(message) { }
internal NotInstalledException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -42,10 +42,10 @@ namespace Velopack.NuGet
{
if (SemanticVersion.TryParse(version, out var parsed)) {
if (parsed < new SemanticVersion(0, 0, 1, parsed.Release)) {
throw new Exception($"Invalid package version '{version}', it must be >= 0.0.1.");
throw new ArgumentException($"Invalid package version '{version}', it must be >= 0.0.1.");
}
} else {
throw new Exception($"Invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.");
throw new ArgumentException($"Invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.");
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Versioning;
using Velopack.Compression;
using Velopack.Exceptions;
using Velopack.Locators;
using Velopack.NuGet;
using Velopack.Sources;
@@ -97,6 +98,7 @@ namespace Velopack
if (source == null) {
throw new ArgumentNullException(nameof(source));
}
Source = source;
Log = logger ?? VelopackApp.DefaultLogger ?? NullLogger.Instance;
Locator = locator ?? VelopackApp.DefaultLocator ?? VelopackLocator.GetDefault(Log);
@@ -149,12 +151,14 @@ namespace Velopack
// 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}).");
Log.Info(
$"Latest remote release is the same version of a different channel, and downgrade is enabled ({installedVer}: {DefaultChannel} -> {Channel}).");
return new UpdateInfo(latestRemoteFull, true);
}
}
Log.Info($"No updates, remote version ({latestRemoteFull.Version}) is not newer than current version ({installedVer}) and / or downgrade is not enabled.");
Log.Info(
$"No updates, remote version ({latestRemoteFull.Version}) is not newer than current version ({installedVer}) and / or downgrade is not enabled.");
return null;
}
@@ -214,6 +218,7 @@ namespace Velopack
// the progress delegate may very likely invoke into the client main thread for UI updates, so
// let's try to reduce the spam. report only on even numbers and only if the progress has changed.
int lastProgress = 0;
void reportProgress(int x)
{
int result = (int) (Math.Round(x / 2d, MidpointRounding.AwayFromZero) * 2d);
@@ -262,7 +267,8 @@ namespace Velopack
Log.Info("Ignoring delta updates (ignoreDeltas parameter)");
} else {
if (deltasCount > 10 || deltasSize > targetRelease.Size) {
Log.Info($"There are too many delta's ({deltasCount} > 10) or the sum of their size ({deltasSize} > {targetRelease.Size}) is too large. " +
Log.Info(
$"There are too many delta's ({deltasCount} > 10) or the sum of their size ({deltasSize} > {targetRelease.Size}) is too large. " +
$"Only full update will be available.");
} else {
using var _1 = TempUtil.GetTempDirectory(out var deltaStagingDir, appTempDir);
@@ -272,13 +278,21 @@ namespace Velopack
EasyZip.ExtractZipToDirectory(Log, basePackagePath, deltaStagingDir);
reportProgress(10);
await DownloadAndApplyDeltaUpdates(deltaStagingDir, updates, x => reportProgress(CoreUtil.CalculateProgress(x, 10, 80)), cancelToken)
await DownloadAndApplyDeltaUpdates(
deltaStagingDir,
updates,
x => reportProgress(CoreUtil.CalculateProgress(x, 10, 80)),
cancelToken)
.ConfigureAwait(false);
reportProgress(80);
Log.Info("Delta updates completed, creating final update package.");
File.Delete(incompleteFile);
await EasyZip.CreateZipFromDirectoryAsync(Log, incompleteFile, deltaStagingDir, x => reportProgress(CoreUtil.CalculateProgress(x, 80, 100)),
await EasyZip.CreateZipFromDirectoryAsync(
Log,
incompleteFile,
deltaStagingDir,
x => reportProgress(CoreUtil.CalculateProgress(x, 80, 100)),
cancelToken: cancelToken).ConfigureAwait(false);
File.Delete(completeFile);
File.Move(incompleteFile, completeFile);
@@ -311,11 +325,12 @@ namespace Velopack
if (zip.UpdateExeBytes == null) {
Log.Error("Update.exe not found in package, skipping extraction.");
} else {
await IoUtil.RetryAsync(async () => {
using var ms = new MemoryStream(zip.UpdateExeBytes);
using var fs = File.Create(updateExe);
await ms.CopyToAsync(fs).ConfigureAwait(false);
}).ConfigureAwait(false);
await IoUtil.RetryAsync(
async () => {
using var ms = new MemoryStream(zip.UpdateExeBytes);
using var fs = File.Create(updateExe);
await ms.CopyToAsync(fs).ConfigureAwait(false);
}).ConfigureAwait(false);
}
} catch (Exception ex) {
Log.Error(ex, "Failed to extract new Update.exe");
@@ -334,7 +349,8 @@ namespace Velopack
/// <param name="updates">An update object containing one or more delta's</param>
/// <param name="progress">A callback reporting process of delta application progress (from 0-100).</param>
/// <param name="cancelToken">A token to use to cancel the request.</param>
protected virtual async Task DownloadAndApplyDeltaUpdates(string extractedBasePackage, UpdateInfo updates, Action<int> progress, CancellationToken cancelToken)
protected virtual async Task DownloadAndApplyDeltaUpdates(string extractedBasePackage, UpdateInfo updates, Action<int> progress,
CancellationToken cancelToken)
{
var releasesToDownload = updates.DeltasToTarget.OrderBy(d => d.Version).ToArray();
@@ -344,22 +360,28 @@ namespace Velopack
// downloading accounts for 0%-50% of progress
double current = 0;
double toIncrement = 100.0 / releasesToDownload.Count();
await releasesToDownload.ForEachAsync(async x => {
var targetFile = Locator.GetLocalPackagePath(x);
double component = 0;
Log.Debug($"Downloading delta version {x.Version}");
await Source.DownloadReleaseEntry(Log, x, targetFile, p => {
lock (progress) {
current -= component;
component = toIncrement / 100.0 * p;
var progressOfStep = (int) Math.Round(current += component);
progress(CoreUtil.CalculateProgress(progressOfStep, 0, 50));
}
}, cancelToken).ConfigureAwait(false);
VerifyPackageChecksum(x, targetFile);
cancelToken.ThrowIfCancellationRequested();
Log.Debug($"Download complete for delta version {x.Version}");
}).ConfigureAwait(false);
await releasesToDownload.ForEachAsync(
async x => {
var targetFile = Locator.GetLocalPackagePath(x);
double component = 0;
Log.Debug($"Downloading delta version {x.Version}");
await Source.DownloadReleaseEntry(
Log,
x,
targetFile,
p => {
lock (progress) {
current -= component;
component = toIncrement / 100.0 * p;
var progressOfStep = (int) Math.Round(current += component);
progress(CoreUtil.CalculateProgress(progressOfStep, 0, 50));
}
},
cancelToken).ConfigureAwait(false);
VerifyPackageChecksum(x, targetFile);
cancelToken.ThrowIfCancellationRequested();
Log.Debug($"Download complete for delta version {x.Version}");
}).ConfigureAwait(false);
Log.Info("All delta packages downloaded and verified, applying them to the base now. The delta staging dir is: " + extractedBasePackage);
@@ -371,10 +393,13 @@ namespace Velopack
var rel = releasesToDownload[i];
double baseProgress = i * progressStepSize;
var packageFile = Locator.GetLocalPackagePath(rel);
builder.ApplyDeltaPackageFast(extractedBasePackage, packageFile, x => {
var progressOfStep = (int) (baseProgress + (progressStepSize * (x / 100d)));
progress(CoreUtil.CalculateProgress(progressOfStep, 50, 100));
});
builder.ApplyDeltaPackageFast(
extractedBasePackage,
packageFile,
x => {
var progressOfStep = (int) (baseProgress + (progressStepSize * (x / 100d)));
progress(CoreUtil.CalculateProgress(progressOfStep, 50, 100));
});
}
progress(100);
@@ -443,7 +468,7 @@ namespace Velopack
if (!hash.Equals(release.SHA1, StringComparison.OrdinalIgnoreCase)) {
throw new ChecksumFailedException(targetPackage.FullName, $"SHA1 doesn't match ({release.SHA1} != {hash}).");
}
}
}
}
/// <summary>
@@ -452,7 +477,7 @@ namespace Velopack
protected virtual void EnsureInstalled()
{
if (AppId == null || !IsInstalled)
throw new Exception("Cannot perform this operation in an application which is not installed.");
throw new NotInstalledException();
}
/// <summary>
@@ -472,6 +497,7 @@ namespace Velopack
if (String.IsNullOrWhiteSpace(urlOrPath)) {
throw new ArgumentException("Must pass a valid URL or file path to UpdateManager", nameof(urlOrPath));
}
if (HttpUtil.IsHttpUrl(urlOrPath)) {
return new SimpleWebSource(urlOrPath, HttpUtil.CreateDefaultDownloader());
} else {
@@ -479,4 +505,4 @@ namespace Velopack
}
}
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Velopack.Exceptions;
namespace Velopack.Util
{
@@ -45,7 +46,7 @@ namespace Velopack.Util
IsLocked = true;
} catch (Exception ex) {
DisposeInternal();
throw new IOException("Failed to acquire exclusive lock file. Is another operation currently running?", ex);
throw new AcquireLockFailedException(ex);
} finally {
_semaphore.Release();
}
@@ -78,6 +79,7 @@ namespace Velopack.Util
if (_fileDescriptor > 0) {
close(_fileDescriptor);
}
var fileBytes = Encoding.UTF8.GetBytes(_filePath).ToArray();
const int O_RDWR = 0x2;
@@ -108,7 +110,7 @@ namespace Velopack.Util
close(fd);
throw new IOException($"lockf failed, errno: {errno}", new Win32Exception(errno));
}
_fileDescriptor = fd;
}
@@ -129,7 +131,7 @@ namespace Velopack.Util
private void DisposeInternal()
{
Interlocked.Exchange(ref this._fileStream, null)?.Dispose();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
if (_fileDescriptor > 0) {
close(_fileDescriptor);

View File

@@ -2,6 +2,7 @@
using NuGet.Versioning;
using Velopack.Compression;
using Velopack.Core;
using Velopack.Exceptions;
using Velopack.Locators;
using Velopack.Sources;
using Velopack.Tests.TestHelpers;