Add nullable annotations to core sdk

This commit is contained in:
Caelan Sayler
2024-01-16 16:41:59 +00:00
parent c4718e4675
commit 34dad9e572
49 changed files with 413 additions and 687 deletions

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using Velopack.NuGet;
namespace Velopack.Packaging.Windows
{
@@ -17,7 +18,7 @@ namespace Velopack.Packaging.Windows
}
[SupportedOSPlatform("windows")]
public static void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
public static void SetPEVersionBlockFromPackageInfo(string exePath, NuspecManifest package, string iconPath = null)
{
var realExePath = Path.GetFullPath(exePath);

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;

View File

@@ -21,7 +21,7 @@ namespace Velopack.Compression
BaseTempDir = baseTmpDir;
}
public void ApplyDeltaPackageFast(string workingPath, string deltaPackageZip, Action<int> progress = null)
public void ApplyDeltaPackageFast(string workingPath, string deltaPackageZip, Action<int>? progress = null)
{
progress = progress ?? (x => { });
@@ -137,7 +137,7 @@ namespace Velopack.Compression
if (File.Exists(finalTarget)) File.Delete(finalTarget);
var targetPath = Directory.GetParent(finalTarget);
var targetPath = Directory.GetParent(finalTarget)!;
if (!targetPath.Exists) targetPath.Create();
File.Move(tempTargetFile, finalTarget);

View File

@@ -18,7 +18,7 @@ namespace Velopack.Compression
ZipFile.ExtractToDirectory(inputFile, outputDirectory);
}
public static void CreateZipFromDirectory(ILogger logger, string outputFile, string directoryToCompress, Action<int> progress = null)
public static void CreateZipFromDirectory(ILogger logger, string outputFile, string directoryToCompress, Action<int>? progress = null)
{
progress ??= (x => { });
logger.Debug($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
@@ -33,7 +33,7 @@ namespace Velopack.Compression
}
}
public static async Task CreateZipFromDirectoryAsync(ILogger logger, string outputFile, string directoryToCompress, Action<int> progress = null)
public static async Task CreateZipFromDirectoryAsync(ILogger logger, string outputFile, string directoryToCompress, Action<int>? progress = null)
{
progress ??= (x => { });
logger.Debug($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");

View File

@@ -1,6 +1,4 @@
#nullable enable
using System;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;

View File

@@ -22,7 +22,7 @@ namespace Velopack
/// Eg. "Invalid {is}. One {what} expected in {in}. None was found."
/// Eg. "Invalid {is}. Only a single {what} expected in {in}. There were 2 or more."
/// </summary>
public static T ContextualSingle<T>(this IEnumerable<T> source, string strIs, string strWhat, string strIn = null)
public static T ContextualSingle<T>(this IEnumerable<T> source, string strIs, string strWhat, string? strIn = null)
{
T result;
using (var e = source.GetEnumerator()) {

View File

@@ -39,6 +39,8 @@ namespace Velopack
psi.UseShellExecute = false;
var p = Process.Start(psi);
if (p == null) throw new Exception("Process.Start returned null.");
p.BeginErrorReadLine();
p.BeginOutputReadLine();
@@ -63,6 +65,8 @@ namespace Velopack
psi.UseShellExecute = false;
var p = Process.Start(psi);
if (p == null) throw new Exception("Process.Start returned null.");
if (!p.WaitForExit(timeoutMs)) {
p.Kill();
throw new TimeoutException("Process did not exit within allotted time.");

View File

@@ -34,7 +34,7 @@ namespace Velopack.Json
Converters = { new JsonStringEnumConverter(), new SemanticVersionConverter() },
};
public static T DeserializeObject<T>(string json)
public static T? DeserializeObject<T>(string json)
{
return JsonSerializer.Deserialize<T>(json, Options);
}
@@ -47,9 +47,11 @@ namespace Velopack.Json
internal class SemanticVersionConverter : JsonConverter<SemanticVersion>
{
public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override SemanticVersion? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return SemanticVersion.Parse(reader.GetString());
var str = reader.GetString();
if (str == null) return null;
return SemanticVersion.Parse(str);
}
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
@@ -76,7 +78,7 @@ namespace Velopack.Json
NullValueHandling = NullValueHandling.Ignore,
};
public static T DeserializeObject<T>(string json)
public static T? DeserializeObject<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json, Options);
}
@@ -89,15 +91,20 @@ namespace Velopack.Json
internal class SemanticVersionConverter : JsonConverter<SemanticVersion>
{
public override SemanticVersion ReadJson(JsonReader reader, Type objectType, SemanticVersion existingValue, bool hasExistingValue, JsonSerializer serializer)
public override SemanticVersion? ReadJson(JsonReader reader, Type objectType, SemanticVersion? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
string s = (string) reader.Value;
string? s = reader.Value as string;
if (s == null) return null;
return SemanticVersion.Parse(s);
}
public override void WriteJson(JsonWriter writer, SemanticVersion value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, SemanticVersion? value, JsonSerializer serializer)
{
writer.WriteValue(value.ToFullString());
if (value != null) {
writer.WriteValue(value.ToFullString());
} else {
writer.WriteNull();
}
}
}

View File

@@ -10,6 +10,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Velopack
{
@@ -56,10 +57,6 @@ namespace Velopack
{
byte[] output = { };
if (content == null) {
goto done;
}
Func<byte[], byte[], bool> matches = (bom, src) => {
if (src.Length < bom.Length) return false;
@@ -86,7 +83,6 @@ namespace Velopack
output = content;
}
done:
if (output.Length > 0) {
Buffer.BlockCopy(content, content.Length - output.Length, output, 0, output.Length);
}
@@ -94,9 +90,9 @@ namespace Velopack
return Encoding.UTF8.GetString(output);
}
public static bool TryParseEnumU16<TEnum>(ushort enumValue, out TEnum retVal)
public static bool TryParseEnumU16<TEnum>(ushort enumValue, out TEnum? retVal)
{
retVal = default(TEnum);
retVal = default;
bool success = Enum.IsDefined(typeof(TEnum), enumValue);
if (success) {
retVal = (TEnum) Enum.ToObject(typeof(TEnum), enumValue);
@@ -148,21 +144,12 @@ namespace Velopack
public static IEnumerable<FileInfo> GetAllFilesRecursively(this DirectoryInfo rootPath)
{
Contract.Requires(rootPath != null);
if (rootPath == null) return Enumerable.Empty<FileInfo>();
return rootPath.EnumerateFiles("*", SearchOption.AllDirectories);
}
public static IEnumerable<string> GetAllFilePathsRecursively(string rootPath)
{
Contract.Requires(rootPath != null);
return Directory.EnumerateFiles(rootPath, "*", SearchOption.AllDirectories);
}
public static string CalculateFileSHA1(string filePath)
{
Contract.Requires(filePath != null);
var bufferSize = 1000000; // 1mb
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize)) {
return CalculateStreamSHA1(stream);
@@ -171,8 +158,6 @@ namespace Velopack
public static string CalculateStreamSHA1(Stream file)
{
Contract.Requires(file != null && file.CanRead);
using (var sha1 = SHA1.Create()) {
return BitConverter.ToString(sha1.ComputeHash(file)).Replace("-", String.Empty);
}
@@ -195,33 +180,18 @@ namespace Velopack
// default RELEASES file name for each platform.
if (VelopackRuntimeInfo.IsOSX) return "RELEASES-osx";
if (VelopackRuntimeInfo.IsLinux) return "RELEASES-linux";
if (VelopackRuntimeInfo.IsWindows) return "RELEASES";
}
// if the channel is an empty string or "win", we use the default RELEASES file name.
if (String.IsNullOrWhiteSpace(channel) || channel.ToLower() == "win") {
return "RELEASES";
} else {
// if the channel is an empty string or "win", we use the default RELEASES file name.
if (String.IsNullOrWhiteSpace(channel) || channel.ToLower() == "win") {
return "RELEASES";
}
// all other cases the RELEASES file includes the channel name.
return $"RELEASES-{channel.ToLower()}";
}
// all other cases the RELEASES file includes the channel name.
return $"RELEASES-{channel.ToLower()}";
}
public static async Task CopyToAsync(string from, string to)
{
Contract.Requires(!String.IsNullOrEmpty(from) && File.Exists(from));
Contract.Requires(!String.IsNullOrEmpty(to));
if (!File.Exists(from)) {
//Log().Warn("The file {0} does not exist", from);
// TODO: should we fail this operation?
return;
}
// XXX: SafeCopy
await Task.Run(() => File.Copy(from, to, true)).ConfigureAwait(false);
}
public static void Retry(this Action block, int retries = 4, int retryDelay = 250, ILogger logger = null)
public static void Retry(this Action block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
{
Retry(() => {
block();
@@ -229,7 +199,7 @@ namespace Velopack
}, retries, retryDelay, logger);
}
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
{
Contract.Requires(retries > 0);
@@ -246,7 +216,7 @@ namespace Velopack
}
}
public static Task RetryAsync(this Func<Task> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
public static Task RetryAsync(this Func<Task> block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
{
return RetryAsync(async () => {
await block().ConfigureAwait(false);
@@ -254,7 +224,7 @@ namespace Velopack
}, retries, retryDelay, logger);
}
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
{
while (true) {
try {
@@ -322,7 +292,7 @@ namespace Velopack
string name = "temp." + i;
var target = Path.Combine(tempDir, name);
FileSystemInfo info = null;
FileSystemInfo? info = null;
if (Directory.Exists(target)) info = new DirectoryInfo(target);
else if (File.Exists(target)) info = new FileInfo(target);
@@ -378,10 +348,11 @@ namespace Velopack
/// <param name="renameFirst">Try to rename this object first before deleting. Can help prevent partial delete of folders.</param>
/// <param name="logger">Logger for diagnostic messages.</param>
/// <returns>True if the file system object was deleted, false otherwise.</returns>
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false, ILogger logger = null)
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false, ILogger? logger = null)
{
logger ??= NullLogger.Instance;
Contract.Requires(!String.IsNullOrEmpty(path));
logger?.Debug($"Starting to delete: {path}");
logger.Debug($"Starting to delete: {path}");
try {
if (File.Exists(path)) {
@@ -402,7 +373,7 @@ namespace Velopack
return true;
} catch (Exception ex) {
logger?.Error(ex, $"Unable to delete '{path}'");
logger.Error(ex, $"Unable to delete '{path}'");
if (throwOnFailure)
throw;
return false;
@@ -426,7 +397,7 @@ namespace Velopack
}
}
} catch (Exception ex) {
logger?.Warn(ex, $"Unable to traverse children of '{fileSystemInfo.FullName}'");
logger.Warn(ex, $"Unable to traverse children of '{fileSystemInfo.FullName}'");
}
// finally, delete myself, we should try this even if deleting children failed
@@ -610,7 +581,7 @@ namespace Velopack
using (var algorithm = SHA1.Create()) {
algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0);
algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length);
hash = algorithm.Hash;
hash = algorithm.Hash!;
}
// most bytes from the hash are copied straight to the bytes of

View File

@@ -10,34 +10,34 @@ namespace Velopack.Locators
public interface IVelopackLocator
{
/// <summary> The unique application Id. This is used in various app paths. </summary>
public string AppId { get; }
public string? AppId { get; }
/// <summary>
/// The root directory of the application. On Windows, this folder contains all
/// the application files, but that may not be the case on other operating systems.
/// </summary>
public string RootAppDir { get; }
public string? RootAppDir { get; }
/// <summary> The directory in which nupkg files are stored for this application. </summary>
public string PackagesDir { get; }
public string? PackagesDir { get; }
/// <summary> The directory in which versioned application files are stored. </summary>
public string AppContentDir { get; }
public string? AppContentDir { get; }
/// <summary> The temporary directory for this application. </summary>
public string AppTempDir { get; }
public string? AppTempDir { get; }
/// <summary> The path to the current Update.exe or similar on other operating systems. </summary>
public string UpdateExePath { get; }
public string? UpdateExePath { get; }
/// <summary> The currently installed version of the application, or null if the app is not installed. </summary>
public SemanticVersion CurrentlyInstalledVersion { get; }
public SemanticVersion? CurrentlyInstalledVersion { get; }
/// <summary> The path from <see cref="AppContentDir"/> to this executable. </summary>
public string ThisExeRelativePath { get; }
public string? ThisExeRelativePath { get; }
/// <summary> The release channel this package was built for. </summary>
public string Channel { get; }
public string? Channel { get; }
/// <summary>
/// Finds .nupkg files in the PackagesDir and returns a list of ReleaseEntryName objects.
@@ -47,7 +47,7 @@ namespace Velopack.Locators
/// <summary>
/// Finds latest .nupkg file in the PackagesDir or null if not found.
/// </summary>
public VelopackAsset GetLatestLocalFullPackage();
public VelopackAsset? GetLatestLocalFullPackage();
/// <summary>
/// Unique identifier for this user which is used to calculate whether this user is eligible for

View File

@@ -20,37 +20,37 @@ namespace Velopack.Locators
public class LinuxVelopackLocator : VelopackLocator
{
/// <inheritdoc />
public override string AppId { get; }
public override string? AppId { get; }
/// <inheritdoc />
public override string RootAppDir { get; }
public override string? RootAppDir { get; }
/// <inheritdoc />
public override string UpdateExePath { get; }
public override string? UpdateExePath { get; }
/// <inheritdoc />
public override SemanticVersion CurrentlyInstalledVersion { get; }
public override SemanticVersion? CurrentlyInstalledVersion { get; }
/// <inheritdoc />
public override string AppContentDir { get; }
public override string? AppContentDir { get; }
/// <inheritdoc />
public override string Channel { get; }
public override string? Channel { get; }
/// <inheritdoc />
public override string AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId);
public override string? AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId);
/// <inheritdoc />
public override string PackagesDir => CreateSubDirIfDoesNotExist(PersistentTempDir, "packages");
public override string? PackagesDir => CreateSubDirIfDoesNotExist(PersistentTempDir, "packages");
/// <summary> /var/tmp/{velopack}/{appid}, for storing app specific files which need to be preserved. </summary>
public string PersistentTempDir => CreateSubDirIfDoesNotExist(PersistentVelopackDir, AppId);
public string? PersistentTempDir => CreateSubDirIfDoesNotExist(PersistentVelopackDir, AppId);
/// <summary> A pointer to /var/tmp/{velopack}, a location on linux which is semi-persistent. </summary>
public string PersistentVelopackDir => CreateSubDirIfDoesNotExist("/var/tmp", "velopack");
public string? PersistentVelopackDir => CreateSubDirIfDoesNotExist("/var/tmp", "velopack");
/// <summary> File path of the .AppImage which mounted and ran this application. </summary>
public string AppImagePath => Environment.GetEnvironmentVariable("APPIMAGE");
public string? AppImagePath => Environment.GetEnvironmentVariable("APPIMAGE");
/// <summary>
/// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the
@@ -63,7 +63,7 @@ namespace Velopack.Locators
throw new NotSupportedException("Cannot instantiate LinuxVelopackLocator on a non-linux system.");
Log.Info($"Initialising {nameof(LinuxVelopackLocator)}");
// are we inside a mounted .AppImage?
var ourPath = VelopackRuntimeInfo.EntryExePath;
var ix = ourPath.IndexOf("/usr/bin/", StringComparison.InvariantCultureIgnoreCase);

View File

@@ -15,28 +15,28 @@ namespace Velopack.Locators
public class OsxVelopackLocator : VelopackLocator
{
/// <inheritdoc />
public override string AppId { get; }
public override string? AppId { get; }
/// <inheritdoc />
public override string RootAppDir { get; }
public override string? RootAppDir { get; }
/// <inheritdoc />
public override string UpdateExePath { get; }
public override string? UpdateExePath { get; }
/// <inheritdoc />
public override SemanticVersion CurrentlyInstalledVersion { get; }
public override SemanticVersion? CurrentlyInstalledVersion { get; }
/// <inheritdoc />
public override string AppContentDir => RootAppDir;
public override string? AppContentDir => RootAppDir;
/// <inheritdoc />
public override string AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId);
public override string? AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId);
/// <inheritdoc />
public override string PackagesDir => CreateSubDirIfDoesNotExist(AppTempDir, "packages");
public override string? PackagesDir => CreateSubDirIfDoesNotExist(AppTempDir, "packages");
/// <inheritdoc />
public override string Channel { get; }
public override string? Channel { get; }
/// <summary>
/// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the

View File

@@ -14,7 +14,7 @@ namespace Velopack.Locators
public class TestVelopackLocator : VelopackLocator
{
/// <inheritdoc />
public override string AppId {
public override string? AppId {
get {
if (_id == null) {
throw new NotSupportedException("AppId is not supported in this test implementation.");
@@ -24,7 +24,7 @@ namespace Velopack.Locators
}
/// <inheritdoc />
public override string RootAppDir {
public override string? RootAppDir {
get {
if (_root == null) {
throw new NotSupportedException("RootAppDir is not supported in this test implementation.");
@@ -34,7 +34,7 @@ namespace Velopack.Locators
}
/// <inheritdoc />
public override string PackagesDir {
public override string? PackagesDir {
get {
if (_packages == null) {
throw new NotSupportedException("PackagesDir is not supported in this test implementation.");
@@ -44,7 +44,7 @@ namespace Velopack.Locators
}
/// <inheritdoc />
public override string UpdateExePath {
public override string? UpdateExePath {
get {
if (_updatePath == null) {
throw new NotSupportedException("UpdateExePath is not supported in this test implementation.");
@@ -54,7 +54,7 @@ namespace Velopack.Locators
}
/// <inheritdoc />
public override SemanticVersion CurrentlyInstalledVersion {
public override SemanticVersion? CurrentlyInstalledVersion {
get {
if (_version == null) {
throw new NotSupportedException("CurrentlyInstalledVersion is not supported in this test implementation.");
@@ -64,7 +64,7 @@ namespace Velopack.Locators
}
/// <inheritdoc />
public override string AppContentDir {
public override string? AppContentDir {
get {
if (_appContent == null) {
throw new NotSupportedException("AppContentDir is not supported in this test implementation.");
@@ -74,29 +74,29 @@ namespace Velopack.Locators
}
/// <inheritdoc />
public override string Channel {
public override string? Channel {
get {
return _channel;
}
}
private readonly string _updatePath;
private readonly SemanticVersion _version;
private readonly string _packages;
private readonly string _id;
private readonly string _root;
private readonly string _appContent;
private readonly string _channel;
private readonly string? _updatePath;
private readonly SemanticVersion? _version;
private readonly string? _packages;
private readonly string? _id;
private readonly string? _root;
private readonly string? _appContent;
private readonly string? _channel;
/// <inheritdoc cref="TestVelopackLocator" />
public TestVelopackLocator(string appId, string version, string packagesDir, ILogger logger = null)
public TestVelopackLocator(string appId, string version, string packagesDir, ILogger? logger = null)
: this(appId, version, packagesDir, null, null, null, null, logger)
{
}
/// <inheritdoc cref="TestVelopackLocator" />
public TestVelopackLocator(string appId, string version, string packagesDir, string appDir,
string rootDir, string updateExe, string channel = null, ILogger logger = null)
public TestVelopackLocator(string appId, string version, string packagesDir, string? appDir,
string? rootDir, string? updateExe, string? channel = null, ILogger? logger = null)
: base(logger)
{
_id = appId;

View File

@@ -15,7 +15,7 @@ namespace Velopack.Locators
/// </summary>
public abstract class VelopackLocator : IVelopackLocator
{
private static VelopackLocator _current;
private static VelopackLocator? _current;
/// <summary>
/// Auto-detect the platform from the current operating system.
@@ -40,29 +40,30 @@ namespace Velopack.Locators
}
/// <inheritdoc/>
public abstract string AppId { get; }
public abstract string? AppId { get; }
/// <inheritdoc/>
public abstract string RootAppDir { get; }
public abstract string? RootAppDir { get; }
/// <inheritdoc/>
public abstract string PackagesDir { get; }
public abstract string? PackagesDir { get; }
/// <inheritdoc/>
public virtual string AppTempDir => CreateSubDirIfDoesNotExist(PackagesDir, "VelopackTemp");
public virtual string? AppTempDir => CreateSubDirIfDoesNotExist(PackagesDir, "VelopackTemp");
/// <inheritdoc/>
public abstract string UpdateExePath { get; }
public abstract string? UpdateExePath { get; }
/// <inheritdoc/>
public abstract string AppContentDir { get; }
public abstract string? AppContentDir { get; }
/// <inheritdoc/>
public abstract string Channel { get; }
public abstract string? Channel { get; }
/// <inheritdoc/>
public virtual string ThisExeRelativePath {
public virtual string? ThisExeRelativePath {
get {
if (AppContentDir == null) return null;
var path = VelopackRuntimeInfo.EntryExePath;
if (path.StartsWith(AppContentDir, StringComparison.OrdinalIgnoreCase)) {
return path.Substring(AppContentDir.Length + 1);
@@ -73,13 +74,13 @@ namespace Velopack.Locators
}
/// <inheritdoc/>
public abstract SemanticVersion CurrentlyInstalledVersion { get; }
public abstract SemanticVersion? CurrentlyInstalledVersion { get; }
/// <summary> The log interface to use for diagnostic messages. </summary>
protected ILogger Log { get; }
/// <inheritdoc cref="VelopackLocator"/>
protected VelopackLocator(ILogger logger)
protected VelopackLocator(ILogger? logger)
{
Log = logger ?? NullLogger.Instance;
}
@@ -92,14 +93,16 @@ namespace Velopack.Locators
return new List<VelopackAsset>(0);
var list = new List<VelopackAsset>();
foreach (var pkg in Directory.EnumerateFiles(PackagesDir, "*.nupkg")) {
try {
var asset = VelopackAsset.FromNupkg(pkg);
if (asset?.Version != null) {
list.Add(asset);
if (PackagesDir != null) {
foreach (var pkg in Directory.EnumerateFiles(PackagesDir, "*.nupkg")) {
try {
var asset = VelopackAsset.FromNupkg(pkg);
if (asset?.Version != null) {
list.Add(asset);
}
} catch (Exception ex) {
Log.Warn(ex, $"Error while reading local package '{pkg}'.");
}
} catch (Exception ex) {
Log.Warn(ex, $"Error while reading local package '{pkg}'.");
}
}
return list;
@@ -110,7 +113,7 @@ namespace Velopack.Locators
}
/// <inheritdoc/>
public VelopackAsset GetLatestLocalFullPackage()
public VelopackAsset? GetLatestLocalFullPackage()
{
return GetLocalPackages()
.OrderByDescending(x => x.Version)
@@ -122,7 +125,7 @@ namespace Velopack.Locators
/// Given a base dir and a directory name, will create a new sub directory of that name.
/// Will return null if baseDir is null, or if baseDir does not exist.
/// </summary>
protected static string CreateSubDirIfDoesNotExist(string baseDir, string newDir)
protected static string? CreateSubDirIfDoesNotExist(string? baseDir, string? newDir)
{
if (String.IsNullOrEmpty(baseDir) || string.IsNullOrEmpty(newDir)) return null;
var infoBase = new DirectoryInfo(baseDir);
@@ -135,6 +138,7 @@ namespace Velopack.Locators
/// <inheritdoc/>
public Guid? GetOrCreateStagedUserId()
{
if (PackagesDir == null) return null;
var stagedUserIdFile = Path.Combine(PackagesDir, ".betaId");
var ret = default(Guid);

View File

@@ -14,25 +14,25 @@ namespace Velopack.Locators
public class WindowsVelopackLocator : VelopackLocator
{
/// <inheritdoc />
public override string AppId { get; }
public override string? AppId { get; }
/// <inheritdoc />
public override string RootAppDir { get; }
public override string? RootAppDir { get; }
/// <inheritdoc />
public override string UpdateExePath { get; }
public override string? UpdateExePath { get; }
/// <inheritdoc />
public override string AppContentDir { get; }
public override string? AppContentDir { get; }
/// <inheritdoc />
public override SemanticVersion CurrentlyInstalledVersion { get; }
public override SemanticVersion? CurrentlyInstalledVersion { get; }
/// <inheritdoc />
public override string PackagesDir => CreateSubDirIfDoesNotExist(RootAppDir, "packages");
public override string? PackagesDir => CreateSubDirIfDoesNotExist(RootAppDir, "packages");
/// <inheritdoc />
public override string Channel { get; }
public override string? Channel { get; }
/// <inheritdoc cref="WindowsVelopackLocator" />
public WindowsVelopackLocator(ILogger logger) : this(VelopackRuntimeInfo.EntryExePath, logger)
@@ -55,7 +55,7 @@ namespace Velopack.Locators
// directory which is NOT containing a sq.version, in which case we need to infer a lot of info.
ourExePath = Path.GetFullPath(ourExePath);
var myDirPath = Path.GetDirectoryName(ourExePath);
string myDirPath = Path.GetDirectoryName(ourExePath)!;
var myDirName = Path.GetFileName(myDirPath);
var possibleUpdateExe = Path.GetFullPath(Path.Combine(myDirPath, "..\\Update.exe"));
var ixCurrent = ourExePath.LastIndexOf("/current/", StringComparison.InvariantCultureIgnoreCase);

View File

@@ -1,25 +0,0 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using NuGet.Versioning;
namespace Velopack.NuGet
{
public interface IPackage
{
string Id { get; }
string ProductName { get; }
string ProductDescription { get; }
string ProductCompany { get; }
string ProductCopyright { get; }
string Language { get; }
SemanticVersion Version { get; }
Uri ProjectUrl { get; }
string ReleaseNotes { get; }
Uri IconUrl { get; }
IEnumerable<string> Tags { get; }
IEnumerable<FrameworkAssemblyReference> FrameworkAssemblies { get; }
IEnumerable<PackageDependencySet> DependencySets { get; }
IEnumerable<string> RuntimeDependencies { get; }
}
}

View File

@@ -1,15 +0,0 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
namespace Velopack.NuGet
{
public interface IPackageFile
{
Uri Key { get; }
string Path { get; }
string EffectivePath { get; }
string TargetFramework { get; }
bool IsLibFile();
bool IsContentFile();
}
}

View File

@@ -1,11 +0,0 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System.Collections.Generic;
namespace Velopack.NuGet
{
public interface IZipPackage : IPackage
{
IEnumerable<string> Frameworks { get; }
IEnumerable<ZipPackageFile> Files { get; }
}
}

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;

View File

@@ -8,34 +8,27 @@ using NuGet.Versioning;
namespace Velopack.NuGet
{
public class NuspecManifest : IPackage
public class NuspecManifest
{
public string ProductName => Title ?? Id;
public string ProductDescription => Description ?? Summary ?? Title ?? Id;
public string ProductCompany => (Authors.Any() ? String.Join(", ", Authors) : Owners) ?? ProductName;
public string ProductCopyright => Copyright ?? "Copyright © " + DateTime.Now.Year.ToString() + " " + ProductCompany;
//public string FilePath { get; private set; }
public string Id { get; private set; }
public SemanticVersion Version { get; private set; }
public Uri ProjectUrl { get; private set; }
public string ReleaseNotes { get; private set; }
public string ReleaseNotesHtml { get; private set; }
public Uri IconUrl { get; private set; }
public string Language { get; private set; }
public IEnumerable<string> Tags { get; private set; } = Enumerable.Empty<string>();
public string? ProductName => Title ?? Id;
public string? ProductDescription => Description ?? Summary ?? Title ?? Id;
public string? ProductCompany => (Authors.Any() ? String.Join(", ", Authors) : Owners) ?? ProductName;
public string? ProductCopyright => Copyright ?? "Copyright © " + DateTime.Now.Year.ToString() + " " + ProductCompany;
public string? Id { get; private set; }
public SemanticVersion? Version { get; private set; }
public Uri? ProjectUrl { get; private set; }
public string? ReleaseNotes { get; private set; }
public string? ReleaseNotesHtml { get; private set; }
public Uri? IconUrl { get; private set; }
public string? Language { get; private set; }
public string? Channel { get; private set; }
public string? Description { get; private set; }
public string? Owners { get; private set; }
public string? Title { get; private set; }
public string? Summary { get; private set; }
public string? Copyright { get; private set; }
public IEnumerable<string> Authors { get; private set; } = Enumerable.Empty<string>();
public IEnumerable<string> RuntimeDependencies { get; private set; } = Enumerable.Empty<string>();
public IEnumerable<FrameworkAssemblyReference> FrameworkAssemblies { get; private set; } = Enumerable.Empty<FrameworkAssemblyReference>();
public IEnumerable<PackageDependencySet> DependencySets { get; private set; } = Enumerable.Empty<PackageDependencySet>();
public string Channel { get; private set; }
protected string Description { get; private set; }
protected IEnumerable<string> Authors { get; private set; } = Enumerable.Empty<string>();
protected string Owners { get; private set; }
protected string Title { get; private set; }
protected string Summary { get; private set; }
protected string Copyright { get; private set; }
private static readonly string[] ExcludePaths = new[] { "_rels", "package" };
@@ -56,7 +49,7 @@ namespace Velopack.NuGet
manifest = ParseFromFile(filePath);
return true;
} catch {
manifest = null;
manifest = null!;
return false;
}
}
@@ -65,14 +58,11 @@ namespace Velopack.NuGet
{
var document = NugetUtil.LoadSafe(manifestStream, ignoreWhiteSpace: true);
var metadataElement = document.Root.ElementsNoNamespace("metadata").FirstOrDefault();
if (metadataElement == null) {
throw new InvalidDataException("Invalid nuspec xml. Required element 'metadata' missing.");
}
var metadataElement = document.Root.ElementsNoNamespace("metadata").FirstOrDefault()
?? throw new InvalidDataException("Invalid nuspec xml. Required element 'metadata' missing.");
var allElements = new HashSet<string>();
XNode node = metadataElement.FirstNode;
XNode? node = metadataElement.FirstNode;
while (node != null) {
var element = node as XElement;
if (element != null) {
@@ -134,15 +124,6 @@ namespace Velopack.NuGet
case "title":
Title = value;
break;
case "tags":
Tags = getCommaDelimitedValue(value);
break;
case "dependencies":
DependencySets = ReadDependencySets(element);
break;
case "frameworkAssemblies":
FrameworkAssemblies = ReadFrameworkAssemblies(element);
break;
// ===
// the following metadata elements are added by velopack and are not
@@ -159,70 +140,6 @@ namespace Velopack.NuGet
}
}
private List<FrameworkAssemblyReference> ReadFrameworkAssemblies(XElement frameworkElement)
{
if (!frameworkElement.HasElements) {
return new List<FrameworkAssemblyReference>(0);
}
return (from element in frameworkElement.ElementsNoNamespace("frameworkAssembly")
let assemblyNameAttribute = element.Attribute("assemblyName")
where assemblyNameAttribute != null && !String.IsNullOrEmpty(assemblyNameAttribute.Value)
select new FrameworkAssemblyReference(
assemblyNameAttribute.Value.SafeTrim(),
ParseFrameworkNames(element.GetOptionalAttributeValue("targetFramework").SafeTrim()))
).ToList();
}
private List<PackageDependencySet> ReadDependencySets(XElement dependenciesElement)
{
if (!dependenciesElement.HasElements) {
return new List<PackageDependencySet>();
}
// Disallow the <dependencies> element to contain both <dependency> and
// <group> child elements. Unfortunately, this cannot be enforced by XSD.
if (dependenciesElement.ElementsNoNamespace("dependency").Any() &&
dependenciesElement.ElementsNoNamespace("group").Any()) {
throw new InvalidDataException("Manifest_DependenciesHasMixedElements");
}
var dependencies = ReadDependencies(dependenciesElement);
if (dependencies.Count > 0) {
// old format, <dependency> is direct child of <dependencies>
var dependencySet = new PackageDependencySet(null, dependencies);
return new List<PackageDependencySet> { dependencySet };
} else {
var groups = dependenciesElement.ElementsNoNamespace("group");
return (from element in groups
let fx = ParseFrameworkNames(element.GetOptionalAttributeValue("targetFramework").SafeTrim())
select new PackageDependencySet(
element.GetOptionalAttributeValue("targetFramework").SafeTrim(),
ReadDependencies(element))).ToList();
}
}
private List<PackageDependency> ReadDependencies(XElement containerElement)
{
// element is <dependency>
return (from element in containerElement.ElementsNoNamespace("dependency")
let idElement = element.Attribute("id")
where idElement != null && !String.IsNullOrEmpty(idElement.Value)
select new PackageDependency(
idElement.Value.SafeTrim(),
element.GetOptionalAttributeValue("version").SafeTrim()
)).ToList();
}
private IEnumerable<string> ParseFrameworkNames(string frameworkNames)
{
if (String.IsNullOrEmpty(frameworkNames)) {
return Enumerable.Empty<string>();
}
return frameworkNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
protected bool IsPackageFile(string partPath)
{
if (Path.GetFileName(partPath).Equals(NugetUtil.ContentTypeFileName, StringComparison.OrdinalIgnoreCase))
@@ -231,7 +148,7 @@ namespace Velopack.NuGet
if (Path.GetExtension(partPath).Equals(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase))
return false;
string directory = Path.GetDirectoryName(partPath);
string directory = Path.GetDirectoryName(partPath)!;
return !ExcludePaths.Any(p => directory.StartsWith(p, StringComparison.OrdinalIgnoreCase));
}
}

View File

@@ -1,108 +0,0 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
namespace Velopack.NuGet
{
public class PackageDependency
{
public PackageDependency(string id)
: this(id, versionSpec: null)
{
}
public PackageDependency(string id, string versionSpec)
{
if (String.IsNullOrEmpty(id)) {
throw new ArgumentException("Argument_Cannot_Be_Null_Or_Empty", "id");
}
Id = id;
VersionSpec = versionSpec;
}
public string Id {
get;
private set;
}
public string VersionSpec {
get;
private set;
}
public override string ToString()
{
if (VersionSpec == null) {
return Id;
}
return Id + " " + VersionSpec;
}
}
public class PackageDependencySet
{
private readonly string _targetFramework;
private readonly ReadOnlyCollection<PackageDependency> _dependencies;
public PackageDependencySet(string targetFramework, IEnumerable<PackageDependency> dependencies)
{
if (dependencies == null) {
throw new ArgumentNullException("dependencies");
}
_targetFramework = targetFramework;
_dependencies = new ReadOnlyCollection<PackageDependency>(dependencies.ToList());
}
public string TargetFramework {
get {
return _targetFramework;
}
}
public ICollection<PackageDependency> Dependencies {
get {
return _dependencies;
}
}
public IEnumerable<string> SupportedFrameworks {
get {
if (TargetFramework == null) {
yield break;
}
yield return TargetFramework;
}
}
}
public class FrameworkAssemblyReference
{
public FrameworkAssemblyReference(string assemblyName)
: this(assemblyName, Enumerable.Empty<string>())
{
}
public FrameworkAssemblyReference(string assemblyName, IEnumerable<string> supportedFrameworks)
{
if (String.IsNullOrEmpty(assemblyName)) {
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "Argument_Cannot_Be_Null_Or_Empty", "assemblyName"));
}
if (supportedFrameworks == null) {
throw new ArgumentNullException("supportedFrameworks");
}
AssemblyName = assemblyName;
SupportedFrameworks = supportedFrameworks;
}
public string AssemblyName { get; private set; }
public IEnumerable<string> SupportedFrameworks { get; private set; }
}
}

View File

@@ -7,32 +7,27 @@ using System.Linq;
namespace Velopack.NuGet
{
public class ZipPackage : NuspecManifest, IZipPackage
public class ZipPackage : NuspecManifest
{
public IEnumerable<string> Frameworks { get; private set; } = Enumerable.Empty<string>();
public IEnumerable<ZipPackageFile> Files { get; private set; } = Enumerable.Empty<ZipPackageFile>();
public byte[] UpdateExeBytes { get; private set; }
public byte[]? UpdateExeBytes { get; private set; }
public string LoadedFromPath { get; private set; }
public ZipPackage(string filePath) : this(File.OpenRead(filePath))
public ZipPackage(string filePath)
{
LoadedFromPath = filePath;
}
private ZipPackage(Stream zipStream, bool leaveOpen = false)
{
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen);
using var zipStream = File.OpenRead(filePath);
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read, false);
using var manifest = GetManifestEntry(zip).Open();
ReadManifest(manifest);
LoadedFromPath = filePath;
Files = GetPackageFiles(zip).ToArray();
Frameworks = GetFrameworks(Files);
UpdateExeBytes = ReadFileToBytes(zip, f => f.FullName.EndsWith("Squirrel.exe"));
UpdateExeBytes = ReadFile(zip, f => f.FullName.EndsWith("Squirrel.exe"));
}
protected byte[] ReadFileToBytes(ZipArchive archive, Func<ZipArchiveEntry, bool> predicate)
protected byte[]? ReadFile(ZipArchive archive, Func<ZipArchiveEntry, bool> predicate)
{
var f = archive.Entries.FirstOrDefault(predicate);
if (f == null)
@@ -68,15 +63,5 @@ namespace Velopack.NuGet
where IsPackageFile(path)
select new ZipPackageFile(uri);
}
private string[] GetFrameworks(IEnumerable<ZipPackageFile> files)
{
return FrameworkAssemblies
.SelectMany(f => f.SupportedFrameworks)
.Concat(files.Select(z => z.TargetFramework))
.Where(f => f != null)
.Distinct()
.ToArray();
}
}
}

View File

@@ -3,7 +3,7 @@ using System;
namespace Velopack.NuGet
{
public class ZipPackageFile : IPackageFile, IEquatable<ZipPackageFile>
public class ZipPackageFile : IEquatable<ZipPackageFile>
{
public Uri Key { get; }
public string EffectivePath { get; }
@@ -31,14 +31,14 @@ namespace Velopack.NuGet
public override int GetHashCode() => Path.GetHashCode();
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
if (obj is ZipPackageFile zpf)
return Equals(zpf);
return false;
}
public bool Equals(ZipPackageFile other)
public bool Equals(ZipPackageFile? other)
{
if (other == null) return false;
return Path.Equals(other.Path);

View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#nullable disable
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
// https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.NETCore.Platforms
using System;

View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#nullable disable
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -187,7 +188,7 @@ namespace Velopack
public static ReleaseEntry FromVelopackAsset(VelopackAsset asset)
{
return new ReleaseEntry(asset.SHA1, asset.FileName, asset.Size ?? 0);
return new ReleaseEntry(asset.SHA1, asset.FileName, asset.Size);
}
/// <summary>

View File

@@ -36,10 +36,10 @@ namespace Velopack.Sources
/// <summary>
/// The Bearer token used in the request.
/// </summary>
protected virtual string Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "Bearer " + AccessToken;
protected virtual string? Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "Bearer " + AccessToken;
/// <inheritdoc />
public GitBase(string repoUrl, string accessToken, bool prerelease, IFileDownloader downloader = null)
public GitBase(string repoUrl, string accessToken, bool prerelease, IFileDownloader? downloader = null)
{
RepoUri = new Uri(repoUrl);
AccessToken = accessToken;
@@ -61,7 +61,7 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset latestLocalRelease = null)
public virtual async Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset? latestLocalRelease = null)
{
var releases = await GetReleases(Prerelease).ConfigureAwait(false);
if (releases == null || releases.Count() == 0) {

View File

@@ -13,7 +13,7 @@ namespace Velopack.Sources
{
/// <summary> The name of this release. </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary> True if this release is a prerelease. </summary>
[JsonPropertyName("prerelease")]
@@ -25,7 +25,7 @@ namespace Velopack.Sources
/// <summary> A list of assets (files) uploaded to this release. </summary>
[JsonPropertyName("assets")]
public GithubReleaseAsset[] Assets { get; set; }
public GithubReleaseAsset[] Assets { get; set; } = new GithubReleaseAsset[0];
}
/// <summary> Describes a asset (file) uploaded to a GitHub release. </summary>
@@ -36,7 +36,7 @@ namespace Velopack.Sources
/// quota and return JSON unless the 'Accept' header is "application/octet-stream".
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; }
public string? Url { get; set; }
/// <summary>
/// The browser URL for this release asset. This does not use API quota,
@@ -45,15 +45,15 @@ namespace Velopack.Sources
/// be used with an appropriate access token.
/// </summary>
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; }
public string? BrowserDownloadUrl { get; set; }
/// <summary> The name of this release asset. </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary> The mime type of this release asset (as detected by GitHub). </summary>
[JsonPropertyName("content_type")]
public string ContentType { get; set; }
public string? ContentType { get; set; }
}
/// <summary>
@@ -78,7 +78,7 @@ namespace Velopack.Sources
/// <param name="downloader">
/// The file downloader used to perform HTTP requests.
/// </param>
public GithubSource(string repoUrl, string accessToken, bool prerelease, IFileDownloader downloader = null)
public GithubSource(string repoUrl, string accessToken, bool prerelease, IFileDownloader? downloader = null)
: base(repoUrl, accessToken, prerelease, downloader)
{
}
@@ -94,6 +94,7 @@ namespace Velopack.Sources
var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization, "application/vnd.github.v3+json").ConfigureAwait(false);
var releases = SimpleJson.DeserializeObject<List<GithubRelease>>(response);
if (releases == null) return new GithubRelease[0];
return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray();
}
@@ -104,22 +105,24 @@ namespace Velopack.Sources
throw new ArgumentException($"No assets found in Github Release '{release.Name}'.");
}
IEnumerable<GithubReleaseAsset> allReleasesFiles = release.Assets.Where(a => a.Name.Equals(assetName, StringComparison.InvariantCultureIgnoreCase));
IEnumerable<GithubReleaseAsset> allReleasesFiles = release.Assets.Where(a => a.Name?.Equals(assetName, StringComparison.InvariantCultureIgnoreCase) == true);
if (allReleasesFiles == null || allReleasesFiles.Count() == 0) {
throw new ArgumentException($"Could not find asset called '{assetName}' in Github Release '{release.Name}'.");
}
var asset = allReleasesFiles.First();
if (String.IsNullOrWhiteSpace(AccessToken)) {
if (String.IsNullOrWhiteSpace(AccessToken) && asset.BrowserDownloadUrl != null) {
// if no AccessToken provided, we use the BrowserDownloadUrl which does not
// count towards the "unauthenticated api request" limit of 60 per hour per IP.
return asset.BrowserDownloadUrl;
} else {
} else if (asset.Url != null) {
// otherwise, we use the regular asset url, which will allow us to retrieve
// assets from private repositories
// https://docs.github.com/en/rest/reference/releases#get-a-release-asset
return asset.Url;
} else {
throw new ArgumentException("Could not find a valid asset url for the specified asset.");
}
}

View File

@@ -18,7 +18,7 @@ namespace Velopack.Sources
/// The name of the release.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// True if this is intended for an upcoming release.
@@ -30,13 +30,13 @@ namespace Velopack.Sources
/// The date which this release was published publically.
/// </summary>
[JsonPropertyName("released_at")]
public DateTime ReleasedAt { get; set; }
public DateTime? ReleasedAt { get; set; }
/// <summary>
/// A container for the assets (files) uploaded to this release.
/// </summary>
[JsonPropertyName("assets")]
public GitlabReleaseAsset Assets { get; set; }
public GitlabReleaseAsset? Assets { get; set; }
}
/// <summary>
@@ -54,7 +54,7 @@ namespace Velopack.Sources
/// A list of asset (file) links.
/// </summary>
[JsonPropertyName("links")]
public GitlabReleaseLink[] Links { get; set; }
public GitlabReleaseLink[] Links { get; set; } = new GitlabReleaseLink[0];
}
/// <summary>
@@ -66,13 +66,13 @@ namespace Velopack.Sources
/// Name of the asset (file) linked.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// The url for the asset. This make use of the Gitlab API.
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; }
public string? Url { get; set; }
/// <summary>
/// A direct url to the asset, via a traditional URl.
@@ -80,14 +80,14 @@ namespace Velopack.Sources
/// This links directly to the raw asset (file).
/// </summary>
[JsonPropertyName("direct_asset_url")]
public string DirectAssetUrl { get; set; }
public string? DirectAssetUrl { get; set; }
/// <summary>
/// The category type that the asset is listed under.
/// Options: 'Package', 'Image', 'Runbook', 'Other'
/// </summary>
[JsonPropertyName("link_type")]
public string Type { get; set; }
public string? Type { get; set; }
}
/// <summary>
@@ -111,7 +111,7 @@ namespace Velopack.Sources
/// <param name="downloader">
/// The file downloader used to perform HTTP requests.
/// </param>
public GitlabSource(string repoUrl, string accessToken, bool upcomingRelease, IFileDownloader downloader = null)
public GitlabSource(string repoUrl, string accessToken, bool upcomingRelease, IFileDownloader? downloader = null)
: base(repoUrl, accessToken, upcomingRelease, downloader)
{
}
@@ -128,16 +128,18 @@ namespace Velopack.Sources
throw new ArgumentException($"No assets found in Gitlab Release '{release.Name}'.");
}
GitlabReleaseLink packageFile =
release.Assets.Links.FirstOrDefault(a => a.Name.Equals(assetName, StringComparison.InvariantCultureIgnoreCase));
GitlabReleaseLink? packageFile =
release.Assets.Links.FirstOrDefault(a => a.Name?.Equals(assetName, StringComparison.InvariantCultureIgnoreCase) == true);
if (packageFile == null) {
throw new ArgumentException($"Could not find asset called '{assetName}' in GitLab Release '{release.Name}'.");
}
if (String.IsNullOrWhiteSpace(AccessToken)) {
if (String.IsNullOrWhiteSpace(AccessToken) && packageFile.DirectAssetUrl != null) {
return packageFile.DirectAssetUrl;
} else {
} else if (packageFile.Url != null) {
return packageFile.Url;
} else {
throw new Exception($"Could not find a valid URL for asset '{assetName}' in GitLab Release '{release.Name}'.");
}
}
@@ -154,6 +156,7 @@ namespace Velopack.Sources
var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization).ConfigureAwait(false);
var releases = SimpleJson.DeserializeObject<List<GitlabRelease>>(response);
if (releases == null) return new GitlabRelease[0];
return releases.OrderByDescending(d => d.ReleasedAt).Where(x => includePrereleases || !x.UpcomingRelease).ToArray();
}
}

View File

@@ -18,7 +18,7 @@ namespace Velopack.Sources
public static ProductInfoHeaderValue UserAgent => new("Velopack", VelopackRuntimeInfo.VelopackNugetVersion.ToFullString());
/// <inheritdoc />
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, string authorization, string accept)
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, string? authorization, string? accept)
{
using var client = CreateHttpClient(authorization, accept);
try {
@@ -35,7 +35,7 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<byte[]> DownloadBytes(string url, string authorization, string accept)
public virtual async Task<byte[]> DownloadBytes(string url, string? authorization, string? accept)
{
using var client = CreateHttpClient(authorization, accept);
try {
@@ -48,7 +48,7 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<string> DownloadString(string url, string authorization, string accept)
public virtual async Task<string> DownloadString(string url, string? authorization, string? accept)
{
using var client = CreateHttpClient(authorization, accept);
try {
@@ -64,7 +64,7 @@ namespace Velopack.Sources
/// Asynchronously downloads a remote url to the specified destination stream while
/// providing progress updates.
/// </summary>
protected virtual async Task DownloadToStreamInternal(HttpClient client, string requestUri, Stream destination, Action<int> progress = null, CancellationToken cancellationToken = default)
protected virtual async Task DownloadToStreamInternal(HttpClient client, string requestUri, Stream destination, Action<int>? progress = null, CancellationToken cancellationToken = default)
{
// https://stackoverflow.com/a/46497896/184746
// Get the http headers first to examine the content length
@@ -119,7 +119,7 @@ namespace Velopack.Sources
/// <summary>
/// Creates a new <see cref="HttpClient"/> for every request.
/// </summary>
protected virtual HttpClient CreateHttpClient(string authorization, string accept)
protected virtual HttpClient CreateHttpClient(string? authorization, string? accept)
{
var client = new HttpClient(CreateHttpClientHandler(), true);
client.DefaultRequestHeaders.UserAgent.Add(UserAgent);

View File

@@ -24,16 +24,16 @@ namespace Velopack.Sources
/// <param name="accept">
/// Text to be sent in the 'Accept' header of the request.
/// </param>
Task DownloadFile(string url, string targetFile, Action<int> progress, string authorization = null, string accept = null);
Task DownloadFile(string url, string targetFile, Action<int> progress, string? authorization = null, string? accept = null);
/// <summary>
/// Returns a byte array containing the contents of the file at the specified url
/// </summary>
Task<byte[]> DownloadBytes(string url, string authorization = null, string accept = null);
Task<byte[]> DownloadBytes(string url, string? authorization = null, string? accept = null);
/// <summary>
/// Returns a string containing the contents of the specified url
/// </summary>
Task<string> DownloadString(string url, string authorization = null, string accept = null);
Task<string> DownloadString(string url, string? authorization = null, string? accept = null);
}
}

View File

@@ -27,7 +27,7 @@ namespace Velopack.Sources
/// <param name="logger">The logger to use for any diagnostic messages.</param>
/// <returns>An array of <see cref="ReleaseEntry"/> objects that are available for download
/// and are applicable to this user.</returns>
Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset latestLocalRelease = null);
Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset? latestLocalRelease = null);
/// <summary>
/// Download the specified <see cref="ReleaseEntry"/> to the provided local file path.

View File

@@ -24,7 +24,7 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset latestLocalRelease = null)
public Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset? latestLocalRelease = null)
{
if (!BaseDirectory.Exists) {
logger.Error($"The local update directory '{BaseDirectory.FullName}' does not exist.");

View File

@@ -21,19 +21,19 @@ namespace Velopack.Sources
public virtual IFileDownloader Downloader { get; }
/// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(string baseUrl, IFileDownloader downloader = null)
public SimpleWebSource(string baseUrl, IFileDownloader? downloader = null)
: this(new Uri(baseUrl), downloader)
{ }
/// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(Uri baseUri, IFileDownloader downloader = null)
public SimpleWebSource(Uri baseUri, IFileDownloader? downloader = null)
{
BaseUri = baseUri;
Downloader = downloader ?? Utility.CreateDefaultDownloader();
}
/// <inheritdoc />
public async Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel = null, Guid? stagingId = null, VelopackAsset latestLocalRelease = null)
public async Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null, VelopackAsset? latestLocalRelease = null)
{
var uri = Utility.AppendPathToUri(BaseUri, Utility.GetVeloReleaseIndexName(channel));
var args = new Dictionary<string, string>();

75
src/Velopack/UpdateExe.cs Normal file
View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Velopack.Locators;
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.ApplyPendingUpdate(bool, string[])"/>, or <see cref="UpdateManager.WaitExitThenApplyPendingUpdate(bool, string[])"/>.
/// </summary>
public static class UpdateExe
{
/// <summary>
/// Runs Update.exe in the current working directory to apply updates, optionally restarting the application.
/// </summary>
/// <param name="silent">If true, no dialogs will be shown during the update process. This could result
/// in an update failing to install, such as when we need to ask the user for permission to install
/// 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="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, ILogger? logger = null)
{
logger ??= NullLogger.Instance;
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = locator.UpdateExePath,
WorkingDirectory = Path.GetDirectoryName(locator.UpdateExePath),
};
var args = new List<string>();
if (silent) args.Add("--silent");
args.Add("apply");
args.Add("--wait");
var entry = locator.GetLatestLocalFullPackage();
if (entry != null && locator.PackagesDir != null) {
var pkg = Path.Combine(locator.PackagesDir, entry.FileName);
if (File.Exists(pkg)) {
args.Add("--package");
args.Add(pkg);
}
}
if (restart) args.Add("--restart");
if (restart && restartArgs != null && restartArgs.Length > 0) {
args.Add("--");
foreach (var a in restartArgs) {
args.Add(a);
}
}
psi.AppendArgumentListSafe(args, out var debugArgs);
logger.Debug($"Restarting app to apply updates. Running: {psi.FileName} {debugArgs}");
var p = Process.Start(psi);
Thread.Sleep(300);
if (p == null) {
throw new Exception("Failed to launch Update.exe process.");
}
if (p.HasExited) {
throw new Exception($"Update.exe process exited too soon ({p.ExitCode}).");
}
logger.Info("Update.exe apply triggered successfully.");
}
}
}

View File

@@ -14,7 +14,7 @@
/// The base release that we are to apply delta updates from. If null, we can try doing a delta update from
/// the currently installed version.
/// </summary>
public VelopackAsset BaseRelease { get; }
public VelopackAsset? BaseRelease { get; }
/// <summary>
/// The list of delta versions between the current version and <see cref="TargetFullRelease"/>.
@@ -25,28 +25,11 @@
/// <summary>
/// Create a new instance of <see cref="UpdateInfo"/>
/// </summary>
public UpdateInfo(VelopackAsset targetRelease, VelopackAsset deltaBaseRelease = null, VelopackAsset[] deltasToTarget = null)
public UpdateInfo(VelopackAsset targetRelease, VelopackAsset? deltaBaseRelease = null, VelopackAsset[]? deltasToTarget = null)
{
TargetFullRelease = targetRelease;
BaseRelease = deltaBaseRelease;
DeltasToTarget = deltasToTarget ?? new VelopackAsset[0];
}
// /// <summary>
// /// Retrieves all the release notes for pending packages (ie. <see cref="ReleasesToApply"/>)
// /// </summary>
// public Dictionary<ReleaseEntry, string> FetchReleaseNotes(ReleaseNotesFormat format)
// {
// return ReleasesToApply
// .SelectMany(x => {
// try {
// var releaseNotes = x.GetReleaseNotes(PackageDirectory, format);
// return EnumerableExtensions.Return(Tuple.Create(x, releaseNotes));
// } catch (Exception ex) {
// return Enumerable.Empty<Tuple<ReleaseEntry, string>>();
// }
// })
// .ToDictionary(k => k.Item1, v => v.Item2);
// }
}
}

View File

@@ -21,23 +21,23 @@ namespace Velopack
public 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;
public virtual string? AppId => Locator.AppId;
/// <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="ApplyPendingUpdate(bool, string[])"/> to be applied. </summary>
public virtual bool IsUpdatePendingRestart {
get {
var latestLocal = Locator.GetLatestLocalFullPackage();
if (latestLocal != null && latestLocal.Version > CurrentVersion)
if (latestLocal != null && CurrentVersion != null && latestLocal.Version > CurrentVersion)
return true;
return false;
}
}
/// <summary> The currently installed app version when you created your release. Null if this is not a currently installed app. </summary>
public virtual SemanticVersion CurrentVersion => Locator.CurrentlyInstalledVersion;
public virtual SemanticVersion? CurrentVersion => Locator.CurrentlyInstalledVersion;
/// <summary> The update source to use when checking for/downloading updates. </summary>
protected IUpdateSource Source { get; }
@@ -60,7 +60,7 @@ namespace Velopack
/// 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)
public UpdateManager(string urlOrPath, string? channel = null, ILogger? logger = null, IVelopackLocator? locator = null)
: this(CreateSimpleSource(urlOrPath), channel, logger, locator)
{
}
@@ -75,24 +75,19 @@ namespace Velopack
/// 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)
: this(logger, locator)
public UpdateManager(IUpdateSource source, string? channel = null, ILogger? logger = null, IVelopackLocator? locator = null)
{
if (source == null) {
throw new ArgumentNullException(nameof(source));
}
Source = source;
Channel = channel;
}
internal UpdateManager(ILogger logger, IVelopackLocator locator)
{
Log = logger ?? VelopackApp.DefaultLogger ?? NullLogger.Instance;
Locator = locator ?? VelopackLocator.GetDefault(Log);
Channel = channel ?? Locator.Channel ?? VelopackRuntimeInfo.SystemOs.GetOsShortName();
}
/// <inheritdoc cref="CheckForUpdatesAsync(CancellationToken)"/>
public UpdateInfo CheckForUpdates()
public UpdateInfo? CheckForUpdates()
{
return CheckForUpdatesAsync()
.ConfigureAwait(false).GetAwaiter().GetResult();
@@ -104,16 +99,15 @@ namespace Velopack
/// </summary>
/// <param name="cancelToken">An optional cancellation token if you wish to stop this operation.</param>
/// <returns>Null if no updates, otherwise <see cref="UpdateInfo"/> containing the version of the latest update available.</returns>
public virtual async Task<UpdateInfo> CheckForUpdatesAsync(CancellationToken cancelToken = default)
public virtual async Task<UpdateInfo?> CheckForUpdatesAsync(CancellationToken cancelToken = default)
{
EnsureInstalled();
var installedVer = CurrentVersion;
var installedVer = CurrentVersion!;
var betaId = Locator.GetOrCreateStagedUserId();
var latestLocalFull = Locator.GetLatestLocalFullPackage();
Log.Debug("Retrieving latest release feed.");
var channel = Channel ?? Locator.Channel ?? VelopackRuntimeInfo.SystemOs.GetOsShortName();
var feedObj = await Source.GetReleaseFeed(Log, channel, betaId, latestLocalFull).ConfigureAwait(false);
var feedObj = await Source.GetReleaseFeed(Log, Channel, betaId, latestLocalFull).ConfigureAwait(false);
var feed = feedObj.Assets;
var latestRemoteFull = feed.Where(r => r.Type == VelopackAssetType.Full).MaxBy(x => x.Version).FirstOrDefault();
@@ -137,7 +131,7 @@ namespace Velopack
// 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;
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 " +
@@ -155,7 +149,7 @@ namespace Velopack
}
/// <inheritdoc cref="DownloadUpdatesAsync(UpdateInfo, Action{int}, bool, CancellationToken)"/>
public void DownloadUpdates(UpdateInfo updates, Action<int> progress = null, bool ignoreDeltas = false)
public void DownloadUpdates(UpdateInfo updates, Action<int>? progress = null, bool ignoreDeltas = false)
{
DownloadUpdatesAsync(updates, progress, ignoreDeltas)
.ConfigureAwait(false).GetAwaiter().GetResult();
@@ -172,9 +166,8 @@ namespace Velopack
/// <param name="ignoreDeltas">Whether to attempt downloading delta's or skip to full package download.</param>
/// <param name="cancelToken">An optional cancellation token if you wish to stop this operation.</param>
public virtual async Task DownloadUpdatesAsync(
UpdateInfo updates, Action<int> progress = null, bool ignoreDeltas = false, CancellationToken cancelToken = default)
UpdateInfo updates, Action<int>? progress = null, bool ignoreDeltas = false, CancellationToken cancelToken = default)
{
progress ??= (_ => { });
// the progress delegate may very likely invoke into the client main thread for UI updates, so
@@ -189,7 +182,11 @@ namespace Velopack
}
}
var targetRelease = updates?.TargetFullRelease;
if (updates == null) {
throw new ArgumentNullException(nameof(updates));
}
var targetRelease = updates.TargetFullRelease;
if (targetRelease == null) {
throw new ArgumentException("Must pass a valid UpdateInfo object with a non-null TargetFullRelease", nameof(updates));
}
@@ -197,7 +194,10 @@ namespace Velopack
EnsureInstalled();
using var _mut = AcquireUpdateLock();
var completeFile = Path.Combine(Locator.PackagesDir, targetRelease.FileName);
var appTempDir = Locator.AppTempDir!;
var appPackageDir = Locator.PackagesDir!;
var completeFile = Path.Combine(appPackageDir, targetRelease.FileName);
var incompleteFile = completeFile + ".partial";
try {
@@ -214,7 +214,7 @@ namespace Velopack
var deltasCount = updates.DeltasToTarget.Count();
try {
if (deltasCount > 0) {
if (updates.BaseRelease?.FileName != null && deltasCount > 0) {
if (ignoreDeltas) {
Log.Info("Ignoring delta updates (ignoreDeltas parameter)");
} else {
@@ -222,16 +222,12 @@ namespace Velopack
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 = Utility.GetTempDirectory(out var deltaStagingDir, Locator.AppTempDir);
if (updates.BaseRelease?.FileName != null) {
string basePackagePath = Path.Combine(Locator.PackagesDir, updates.BaseRelease.FileName);
if (!File.Exists(basePackagePath))
throw new Exception($"Unable to find base package {basePackagePath} for delta update.");
EasyZip.ExtractZipToDirectory(Log, basePackagePath, deltaStagingDir);
} else {
Log.Warn("No base package available. Attempting delta update using application files.");
Utility.CopyFiles(Locator.AppContentDir, deltaStagingDir);
}
using var _1 = Utility.GetTempDirectory(out var deltaStagingDir, appTempDir);
string basePackagePath = Path.Combine(appPackageDir, updates.BaseRelease.FileName);
if (!File.Exists(basePackagePath))
throw new Exception($"Unable to find base package {basePackagePath} for delta update.");
EasyZip.ExtractZipToDirectory(Log, basePackagePath, deltaStagingDir);
reportProgress(10);
await DownloadAndApplyDeltaUpdates(deltaStagingDir, updates, x => reportProgress(Utility.CalculateProgress(x, 10, 80)))
.ConfigureAwait(false);
@@ -268,7 +264,7 @@ namespace Velopack
} finally {
if (VelopackRuntimeInfo.IsWindows) {
try {
var updateExe = Locator.UpdateExePath;
var updateExe = Locator.UpdateExePath!;
Log.Info("Extracting new Update.exe to " + updateExe);
var zip = new ZipPackage(completeFile);
@@ -291,24 +287,16 @@ namespace Velopack
}
/// <summary>
/// This will exit your app immediately and then apply updates.
/// </summary>
/// <param name="silent"></param>
public void ApplyUpdatesAndExit(bool silent = false)
{
RunApplyUpdates(silent, false, null);
Environment.Exit(0);
}
/// <summary>
/// This will exit your app immediately, apply updates, and 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.
/// 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="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 ApplyUpdatesAndRestart(string[] restartArgs = null)
public void ApplyPendingUpdate(bool restart = true, string[]? restartArgs = null)
{
RunApplyUpdates(false, true, restartArgs);
WaitExitThenApplyPendingUpdate(restart, restartArgs);
Environment.Exit(0);
}
@@ -316,81 +304,35 @@ namespace Velopack
/// 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>
public void QueueApplyAndWait(bool restart = true, string[] restartArgs = null)
{
RunApplyUpdates(false, restart, restartArgs);
}
/// <summary>
/// Runs Update.exe in the current working directory to apply updates, optionally restarting the application.
/// </summary>
/// <param name="silent">If true, no dialogs will be shown during the update process. This could result
/// in an update failing to install, such as when we need to ask the user for permission to install
/// a new framework dependency.</param>
/// <param name="restart">If true, restarts the application after updates are applied (or if they failed)</param>
/// <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>
/// <exception cref="Exception"></exception>
protected virtual void RunApplyUpdates(bool silent, bool restart, string[] restartArgs)
public void WaitExitThenApplyPendingUpdate(bool restart = true, string[]? restartArgs = null)
{
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = Locator.UpdateExePath,
WorkingDirectory = Path.GetDirectoryName(Locator.UpdateExePath),
};
var args = new List<string>();
if (silent) args.Add("--silent");
args.Add("apply");
args.Add("--wait");
var entry = Locator.GetLatestLocalFullPackage();
if (entry != null) {
var pkg = Path.Combine(Locator.PackagesDir, entry.FileName);
if (File.Exists(pkg)) {
args.Add("--package");
args.Add(pkg);
}
}
if (restart) args.Add("--restart");
if (restart && restartArgs != null && restartArgs.Length > 0) {
args.Add("--");
foreach (var a in restartArgs) {
args.Add(a);
}
}
psi.AppendArgumentListSafe(args, out var debugArgs);
Log.Debug($"Restarting app to apply updates. Running: {psi.FileName} {debugArgs}");
var p = Process.Start(psi);
Thread.Sleep(300);
if (p == null) {
throw new Exception("Failed to launch Update.exe process.");
}
if (p.HasExited) {
throw new Exception($"Update.exe process exited too soon ({p.ExitCode}).");
}
Log.Info("Update.exe apply triggered successfully.");
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.
/// Given a folder containing the extracted base package, and a list of delta updates, downloads and applies the
/// delta updates to the base package.
/// </summary>
/// <param name="extractedBasePackage">A folder containing the application files to apply the delta's to.</param>
/// <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>
protected virtual async Task DownloadAndApplyDeltaUpdates(string extractedBasePackage, UpdateInfo updates, Action<int> progress)
{
var packagesDirectory = Locator.PackagesDir;
var releasesToDownload = updates.DeltasToTarget.OrderBy(d => d.Version).ToArray();
var appTempDir = Locator.AppTempDir!;
var appPackageDir = Locator.PackagesDir!;
var updateExe = Locator.UpdateExePath!;
// downloading accounts for 0%-50% of progress
double current = 0;
double toIncrement = 100.0 / releasesToDownload.Count();
await releasesToDownload.ForEachAsync(async x => {
var targetFile = Path.Combine(packagesDirectory, x.FileName);
var targetFile = Path.Combine(appPackageDir, x.FileName);
double component = 0;
Log.Debug($"Downloading delta version {x.Version}");
await Source.DownloadReleaseEntry(Log, x, targetFile, p => {
@@ -409,11 +351,11 @@ namespace Velopack
// applying deltas accounts for 50%-100% of progress
double progressStepSize = 100d / releasesToDownload.Length;
var builder = new DeltaUpdateExe(Log, Locator.AppTempDir, Locator.UpdateExePath);
var builder = new DeltaUpdateExe(Log, appTempDir, updateExe);
for (var i = 0; i < releasesToDownload.Length; i++) {
var rel = releasesToDownload[i];
double baseProgress = i * progressStepSize;
var packageFile = Path.Combine(packagesDirectory, rel.FileName);
var packageFile = Path.Combine(appPackageDir, rel.FileName);
builder.ApplyDeltaPackageFast(extractedBasePackage, packageFile, x => {
var progressOfStep = (int) (baseProgress + (progressStepSize * (x / 100d)));
progress(Utility.CalculateProgress(progressOfStep, 50, 100));
@@ -429,11 +371,12 @@ namespace Velopack
protected void CleanIncompleteAndDeltaPackages()
{
try {
var appPackageDir = Locator.PackagesDir!;
Log.Info("Cleaning up incomplete and delta packages from packages directory.");
foreach (var l in Locator.GetLocalPackages()) {
if (l.Type == VelopackAssetType.Delta) {
try {
var pkgPath = Path.Combine(Locator.PackagesDir, l.FileName);
var pkgPath = Path.Combine(appPackageDir, l.FileName);
File.Delete(pkgPath);
Log.Trace(pkgPath + " deleted.");
} catch (Exception ex) {
@@ -442,7 +385,7 @@ namespace Velopack
}
}
foreach (var l in Directory.EnumerateFiles(Locator.PackagesDir, "*.partial").ToArray()) {
foreach (var l in Directory.EnumerateFiles(appPackageDir, "*.partial").ToArray()) {
try {
File.Delete(l);
Log.Trace(l + " deleted.");
@@ -460,10 +403,10 @@ namespace Velopack
/// </summary>
/// <param name="release">The entry to check</param>
/// <param name="filePathOverride">Optional file path, if not specified the package will be loaded from %pkgdir%/release.OriginalFilename.</param>
protected internal virtual void VerifyPackageChecksum(VelopackAsset release, string filePathOverride = null)
protected internal virtual void VerifyPackageChecksum(VelopackAsset release, string? filePathOverride = null)
{
var targetPackage = filePathOverride == null
? new FileInfo(Path.Combine(Locator.PackagesDir, release.FileName))
? new FileInfo(Path.Combine(Locator.PackagesDir!, release.FileName))
: new FileInfo(filePathOverride);
if (!targetPackage.Exists) {
@@ -496,7 +439,7 @@ namespace Velopack
{
var mutexId = $"velopack-{AppId}";
bool created = false;
Mutex mutex = null;
Mutex? mutex = null;
try {
mutex = new Mutex(false, mutexId, out created);
} catch (Exception ex) {

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net462;net48;netstandard2.0;net6.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<CheckEolTargetFramework>false</CheckEolTargetFramework>

View File

@@ -24,14 +24,15 @@ namespace Velopack
{
internal static ILogger DefaultLogger { get; private set; } = NullLogger.Instance;
IVelopackLocator _locator;
VelopackHook _install;
VelopackHook _update;
VelopackHook _obsolete;
VelopackHook _uninstall;
VelopackHook _firstrun;
VelopackHook _restarted;
string[] _args;
internal static IVelopackLocator? DefaultLocator { get; private set; }
VelopackHook? _install;
VelopackHook? _update;
VelopackHook? _obsolete;
VelopackHook? _uninstall;
VelopackHook? _firstrun;
VelopackHook? _restarted;
string[]? _args;
bool _autoApply = true;
private VelopackApp()
@@ -64,10 +65,11 @@ namespace Velopack
/// <summary>
/// Override the default <see cref="IVelopackLocator"/> used to search for application paths.
/// This will be cached and potentially re-used throughout the lifetime of the application.
/// </summary>
public VelopackApp SetLocator(IVelopackLocator locator)
{
_locator = locator;
DefaultLocator = locator;
return this;
}
@@ -146,7 +148,7 @@ namespace Velopack
/// </summary>
/// <param name="logger">A logging interface for diagnostic messages. This will be
/// cached and potentially re-used throughout the lifetime of the application.</param>
public void Run(ILogger logger = null)
public void Run(ILogger? logger = null)
{
var args = _args ?? Environment.GetCommandLineArgs().Skip(1).ToArray();
@@ -158,7 +160,7 @@ namespace Velopack
}
var log = logger ?? NullLogger.Instance;
var locator = _locator ?? VelopackLocator.GetDefault(log);
var locator = DefaultLocator ?? VelopackLocator.GetDefault(log);
DefaultLogger = log;
log.Info("Starting Velopack App (Run).");
@@ -193,6 +195,10 @@ namespace Velopack
// some initial setup/state
var myVersion = locator.CurrentlyInstalledVersion;
if (myVersion == null) {
return;
}
var firstrun = !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("VELOPACK_FIRSTRUN"));
var restarted = !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("VELOPACK_RESTART"));
var localPackages = locator.GetLocalPackages();
@@ -204,23 +210,26 @@ 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...");
var um = new UpdateManager(log, locator);
um.ApplyUpdatesAndRestart(args);
UpdateExe.Apply(locator, true, true, args, log);
} else {
log.Info("Pre-condition failed, we will not restart to apply updates. (restarted: " + restarted + ", autoApply: " + _autoApply + ")");
}
}
// clean up old versions of the app
var pkgPath = locator.PackagesDir;
foreach (var package in localPackages) {
if (package.Type == VelopackAssetType.Full && (package.Version == latestLocal.Version || package.Version == myVersion)) {
continue;
}
try {
log.Info("Removing old package: " + package.FileName);
var p = Path.Combine(pkgPath, package.FileName);
File.Delete(p);
} catch (Exception ex) {
log.Error(ex, $"Failed to remove old package '{package.FileName}'");
if (pkgPath != null) {
foreach (var package in localPackages) {
if (package.Type == VelopackAssetType.Full && (package.Version == latestLocal?.Version || package.Version == myVersion)) {
continue;
}
try {
log.Info("Removing old package: " + package.FileName);
var p = Path.Combine(pkgPath, package.FileName);
File.Delete(p);
} catch (Exception ex) {
log.Error(ex, $"Failed to remove old package '{package.FileName}'");
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.IO;
using NuGet.Versioning;
using Velopack.Json;
@@ -32,7 +33,7 @@ namespace Velopack
/// </summary>
public static VelopackAssetFeed FromJson(string json)
{
return SimpleJson.DeserializeObject<VelopackAssetFeed>(json);
return SimpleJson.DeserializeObject<VelopackAssetFeed>(json) ?? new VelopackAssetFeed();
}
}
@@ -57,7 +58,7 @@ namespace Velopack
public string SHA1 { get; init; }
/// <summary> The size in bytes of the update package containing this release. </summary>
public long? Size { get; init; }
public long Size { get; init; }
/// <summary> The release notes in markdown format, as passed to Velopack when packaging the release. </summary>
public string NotesMarkdown { get; init; }

View File

@@ -1,8 +1,7 @@
using System;
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using NuGet.Versioning;

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.Linq;
namespace Velopack.Windows

View File

@@ -1,6 +1,8 @@
#pragma warning disable CS1591
#nullable disable
#pragma warning disable CS1591
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
@@ -13,6 +15,7 @@ namespace Velopack.Windows
/// Summary description for ShellLink.
/// </summary>
[SupportedOSPlatform("windows")]
[ExcludeFromCodeCoverage]
public class ShellLink : IDisposable
{
[ComImport()]
@@ -932,6 +935,7 @@ namespace Velopack.Windows
/// Enables extraction of icons for any file type from
/// the Shell.
/// </summary>
[ExcludeFromCodeCoverage]
[SupportedOSPlatform("windows")]
public class FileIcon
{

View File

@@ -1,4 +1,5 @@
using System;
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -222,9 +223,9 @@ namespace Velopack.Windows
}
/// <summary>
/// Given an <see cref="IPackage"/> and <see cref="FileVersionInfo"/> return the target shortcut path.
/// Given an <see cref="ZipPackage"/> and <see cref="FileVersionInfo"/> return the target shortcut path.
/// </summary>
protected virtual string LinkPathForVersionInfo(ShortcutLocation location, IPackage package, FileVersionInfo versionInfo, string rootdir)
protected virtual string LinkPathForVersionInfo(ShortcutLocation location, ZipPackage package, FileVersionInfo versionInfo, string rootdir)
{
var possibleProductNames = new[] {
versionInfo.ProductName,

View File

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

View File

@@ -43,6 +43,7 @@ namespace Velopack.Tests.TestHelpers
Version = maxDeltaVer.Version,
Type = VelopackAssetType.Full,
FileName = $"{maxfullVer.PackageId}-{maxDeltaVer.Version}-full.nupkg",
Size = maxfullVer.Filesize,
});
}
}

View File

@@ -235,7 +235,7 @@ namespace Velopack.Tests
var repo = new FakeFixtureRepository(id, true);
var source = new SimpleWebSource("http://any.com", repo);
var feed = await source.GetReleaseFeed(logger);
var feed = await source.GetReleaseFeed(logger, VelopackRuntimeInfo.SystemOs.GetOsShortName());
var basePkg = feed.Assets
.Where(x => x.Type == VelopackAssetType.Full)
.Single(x => x.Version == SemanticVersion.Parse(fromVersion));

View File

@@ -17,12 +17,10 @@ namespace Velopack.Tests
File.Copy(inputPackage, copyPackage);
var zp = new ZipPackage(inputPackage);
var zipfw = zp.Frameworks;
var zipf = zp.Files.OrderBy(f => f.Path).ToArray();
var zipfLib = zp.Files.Where(f => f.IsLibFile()).OrderBy(f => f.Path).ToArray();
using Package package = Package.Open(copyPackage);
var packagingfw = GetSupportedFrameworks(zp, package);
var packaging = GetFiles(package).OrderBy(f => f.Path).ToArray();
var packagingLib = GetLibFiles(package).OrderBy(f => f.Path).ToArray();
@@ -31,7 +29,6 @@ namespace Velopack.Tests
// throw new Exception();
//}
Assert.Equal(packagingfw, zipfw);
Assert.Equal(packaging, zipf);
Assert.Equal(packagingLib, zipfLib);
}
@@ -55,53 +52,24 @@ namespace Velopack.Tests
Assert.Equal("Copyright ©", dyn.Copyright);
Assert.Equal("en-US", zp.Language);
Assert.Equal("Squirrel for Windows", dyn.Title);
Assert.NotEmpty(zp.DependencySets);
var net461 = zp.DependencySets.First();
Assert.Equal(new[] { ".NETFramework4.6.1" }, net461.SupportedFrameworks);
Assert.Equal(".NETFramework4.6.1", net461.TargetFramework);
Assert.NotEmpty(net461.Dependencies);
var dvt = net461.Dependencies.First();
Assert.Equal("System.ValueTuple", dvt.Id);
Assert.Equal("4.5.0", dvt.VersionSpec);
Assert.Equal(new[] { "net5.0" }, zp.DependencySets.Last().SupportedFrameworks);
Assert.NotEmpty(zp.FrameworkAssemblies);
var fw = zp.FrameworkAssemblies.First();
Assert.Equal("System.Net.Http", fw.AssemblyName);
Assert.Equal(new[] { ".NETFramework4.6.1" }, fw.SupportedFrameworks);
}
IEnumerable<string> GetSupportedFrameworks(ZipPackage zp, Package package)
{
var fileFrameworks = from part in package.GetParts()
where IsPackageFile(part)
select NugetUtil.ParseFrameworkNameFromFilePath(NugetUtil.GetPath(part.Uri), out var effectivePath);
return zp.FrameworkAssemblies.SelectMany(f => f.SupportedFrameworks)
.Concat(fileFrameworks)
.Where(f => f != null)
.Distinct();
}
IEnumerable<IPackageFile> GetLibFiles(Package package)
IEnumerable<ZipPackageFile> GetLibFiles(Package package)
{
return GetFiles(package, NugetUtil.LibDirectory);
}
IEnumerable<IPackageFile> GetFiles(Package package, string directory)
IEnumerable<ZipPackageFile> GetFiles(Package package, string directory)
{
string folderPrefix = directory + Path.DirectorySeparatorChar;
return GetFiles(package).Where(file => file.Path.StartsWith(folderPrefix, StringComparison.OrdinalIgnoreCase));
}
List<IPackageFile> GetFiles(Package package)
List<ZipPackageFile> GetFiles(Package package)
{
return (from part in package.GetParts()
where IsPackageFile(part)
select (IPackageFile) new ZipPackageFile(part.Uri)).ToList();
select new ZipPackageFile(part.Uri)).ToList();
}
bool IsPackageFile(PackagePart part)