Add new IProcessImpl abstraction to C# library

This commit is contained in:
Caelan Sayler
2025-05-09 17:18:36 +01:00
parent 9573a511b5
commit 1177ec864e
11 changed files with 250 additions and 163 deletions

View File

@@ -0,0 +1,69 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Velopack.Logging;
using Velopack.Util;
namespace Velopack.Locators
{
public class DefaultProcessImpl : IProcessImpl
{
[DllImport("user32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AllowSetForegroundWindow(int dwProcessId);
private Process? _currentProcess;
private readonly IVelopackLogger _logger;
public DefaultProcessImpl(IVelopackLogger logger)
{
_logger = logger;
}
public string GetCurrentProcessPath()
{
_currentProcess ??= Process.GetCurrentProcess();
var fileName = _currentProcess.MainModule?.FileName ??
throw new InvalidOperationException($"Could not determine process path, please construct {nameof(IVelopackLocator)} manually.");
return Path.GetFullPath(fileName);
}
public uint GetCurrentProcessId()
{
_currentProcess ??= Process.GetCurrentProcess();
return (uint) _currentProcess.Id;
}
public void StartProcess(string exePath, IEnumerable<string> args, string? workDir, bool showWindow)
{
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = exePath,
WorkingDirectory = workDir,
};
psi.AppendArgumentListSafe(args, out var debugArgs);
_logger.Debug($"Running: {psi.FileName} {debugArgs}");
var p = Process.Start(psi);
if (p == null) {
throw new Exception("Failed to launch process.");
}
if (VelopackRuntimeInfo.IsWindows) {
try {
// this is an attempt to work around a bug where the restarted app fails to come to foreground.
if (!AllowSetForegroundWindow(p.Id))
throw new Win32Exception();
} catch (Exception ex) {
_logger.LogWarning(ex, "Failed to allow Update.exe to set foreground window.");
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System.Collections;
using System.Collections.Generic;
namespace Velopack.Locators
{
public interface IProcessImpl
{
public string GetCurrentProcessPath();
public uint GetCurrentProcessId();
public void StartProcess(string exePath, IEnumerable<string> args, string workDir, bool showWindow);
}
}

View File

@@ -52,17 +52,11 @@ namespace Velopack.Locators
bool IsPortable { get; } bool IsPortable { get; }
/// <summary> /// <summary>
/// The process for which the Velopack Locator has been constructed. This should usually be the current process path. /// Provides an abstraction for dealing with the dotnet Process API. This is used to start processes and
/// get information about the current process.
/// </summary> /// </summary>
string ProcessExePath { get; } IProcessImpl Process { get; }
/// <summary>
/// The process ID for which the Velopack Locator has been constructed. This should usually be the current process ID.
/// Setting this to zero will disable some features of Velopack (like the ability to wait for the process to exit
/// before installing updates).
/// </summary>
uint ProcessId { get; }
/// <summary> /// <summary>
/// Finds .nupkg files in the PackagesDir and returns a list of ReleaseEntryName objects. /// Finds .nupkg files in the PackagesDir and returns a list of ReleaseEntryName objects.
/// </summary> /// </summary>

View File

@@ -24,6 +24,9 @@ namespace Velopack.Locators
/// <inheritdoc /> /// <inheritdoc />
public override string? UpdateExePath { get; } public override string? UpdateExePath { get; }
/// <inheritdoc />
public override IProcessImpl Process { get; }
/// <inheritdoc /> /// <inheritdoc />
public override SemanticVersion? CurrentlyInstalledVersion { get; } public override SemanticVersion? CurrentlyInstalledVersion { get; }
@@ -51,28 +54,23 @@ namespace Velopack.Locators
/// <summary> File path of the .AppImage which mounted and ran this application. </summary> /// <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");
/// <inheritdoc />
public override uint ProcessId { get; }
/// <inheritdoc />
public override string ProcessExePath { get; }
/// <summary> /// <summary>
/// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the /// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the
/// app information from metadata embedded in the .app. /// app information from metadata embedded in the .app.
/// </summary> /// </summary>
public LinuxVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? customLog) public LinuxVelopackLocator(IProcessImpl? processImpl, IVelopackLogger? customLog)
{ {
if (!VelopackRuntimeInfo.IsLinux) if (!VelopackRuntimeInfo.IsLinux)
throw new NotSupportedException($"Cannot instantiate {nameof(LinuxVelopackLocator)} on a non-linux system."); throw new NotSupportedException($"Cannot instantiate {nameof(LinuxVelopackLocator)} on a non-linux system.");
ProcessId = currentProcessId;
var ourPath = ProcessExePath = currentProcessPath;
var combinedLog = new CombinedVelopackLogger(); var combinedLog = new CombinedVelopackLogger();
combinedLog.Add(customLog); combinedLog.Add(customLog);
Log = combinedLog; Log = combinedLog;
Process = processImpl ??= new DefaultProcessImpl(combinedLog);
var ourPath = processImpl.GetCurrentProcessPath();
var currentProcessId = processImpl.GetCurrentProcessId();
using var initLog = new CachedVelopackLogger(combinedLog); using var initLog = new CachedVelopackLogger(combinedLog);
initLog.Info($"Initializing {nameof(LinuxVelopackLocator)}"); initLog.Info($"Initializing {nameof(LinuxVelopackLocator)}");
var logFilePath = Path.Combine(Path.GetTempPath(), DefaultLoggingFileName); var logFilePath = Path.Combine(Path.GetTempPath(), DefaultLoggingFileName);

View File

@@ -24,6 +24,9 @@ namespace Velopack.Locators
/// <inheritdoc /> /// <inheritdoc />
public override string? UpdateExePath { get; } public override string? UpdateExePath { get; }
/// <inheritdoc />
public override IProcessImpl Process { get; }
/// <inheritdoc /> /// <inheritdoc />
public override SemanticVersion? CurrentlyInstalledVersion { get; } public override SemanticVersion? CurrentlyInstalledVersion { get; }
@@ -48,27 +51,22 @@ namespace Velopack.Locators
/// <inheritdoc /> /// <inheritdoc />
public override string? Channel { get; } public override string? Channel { get; }
/// <inheritdoc />
public override uint ProcessId { get; }
/// <inheritdoc />
public override string ProcessExePath { get; }
/// <summary> /// <summary>
/// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the /// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the
/// app information from metadata embedded in the .app. /// app information from metadata embedded in the .app.
/// </summary> /// </summary>
public OsxVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? customLog) public OsxVelopackLocator(IProcessImpl? processImpl, IVelopackLogger? customLog)
{ {
if (!VelopackRuntimeInfo.IsOSX) if (!VelopackRuntimeInfo.IsOSX)
throw new NotSupportedException($"Cannot instantiate {nameof(OsxVelopackLocator)} on a non-osx system."); throw new NotSupportedException($"Cannot instantiate {nameof(OsxVelopackLocator)} on a non-osx system.");
ProcessId = currentProcessId;
var ourPath = ProcessExePath = currentProcessPath;
var combinedLog = new CombinedVelopackLogger(); var combinedLog = new CombinedVelopackLogger();
combinedLog.Add(customLog); combinedLog.Add(customLog);
Log = combinedLog; Log = combinedLog;
Process = processImpl ??= new DefaultProcessImpl(combinedLog);
var ourPath = processImpl.GetCurrentProcessPath();
var currentProcessId = processImpl.GetCurrentProcessId();
using var initLog = new CachedVelopackLogger(combinedLog); using var initLog = new CachedVelopackLogger(combinedLog);
initLog.Info($"Initializing {nameof(OsxVelopackLocator)}"); initLog.Info($"Initializing {nameof(OsxVelopackLocator)}");

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using NuGet.Versioning; using NuGet.Versioning;
using Velopack.Logging; using Velopack.Logging;
@@ -13,7 +14,7 @@ namespace Velopack.Locators
/// having an installed application. This could be used in a CI/CD pipeline, or unit tests etc. /// having an installed application. This could be used in a CI/CD pipeline, or unit tests etc.
/// </summary> /// </summary>
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class TestVelopackLocator : VelopackLocator public class TestVelopackLocator : VelopackLocator, IProcessImpl
{ {
public override string? AppId { public override string? AppId {
get { get {
@@ -83,6 +84,8 @@ namespace Velopack.Locators
public override IVelopackLogger Log { get; } public override IVelopackLogger Log { get; }
public override IProcessImpl Process => this;
public override VelopackAsset? GetLatestLocalFullPackage() public override VelopackAsset? GetLatestLocalFullPackage()
{ {
if (_asset != null) { if (_asset != null) {
@@ -92,10 +95,6 @@ namespace Velopack.Locators
return base.GetLatestLocalFullPackage(); return base.GetLatestLocalFullPackage();
} }
public override uint ProcessId => 0;
public override string ProcessExePath { get; }
private readonly string? _updatePath; private readonly string? _updatePath;
private readonly SemanticVersion? _version; private readonly SemanticVersion? _version;
private readonly string? _packages; private readonly string? _packages;
@@ -104,6 +103,7 @@ namespace Velopack.Locators
private readonly string? _appContent; private readonly string? _appContent;
private readonly string? _channel; private readonly string? _channel;
private readonly VelopackAsset? _asset; private readonly VelopackAsset? _asset;
private readonly string? _processPath;
/// <inheritdoc cref="TestVelopackLocator" /> /// <inheritdoc cref="TestVelopackLocator" />
public TestVelopackLocator(string appId, string version, string packagesDir, IVelopackLogger? logger = null) public TestVelopackLocator(string appId, string version, string packagesDir, IVelopackLogger? logger = null)
@@ -113,7 +113,8 @@ namespace Velopack.Locators
/// <inheritdoc cref="TestVelopackLocator" /> /// <inheritdoc cref="TestVelopackLocator" />
public TestVelopackLocator(string appId, string version, string packagesDir, string? appDir, public TestVelopackLocator(string appId, string version, string packagesDir, string? appDir,
string? rootDir, string? updateExe, string? channel = null, IVelopackLogger? logger = null, VelopackAsset? localPackage = null, string processPath = null!) string? rootDir, string? updateExe, string? channel = null, IVelopackLogger? logger = null, VelopackAsset? localPackage = null,
string processPath = null!)
{ {
_id = appId; _id = appId;
_packages = packagesDir; _packages = packagesDir;
@@ -123,8 +124,23 @@ namespace Velopack.Locators
_appContent = appDir; _appContent = appDir;
_channel = channel; _channel = channel;
_asset = localPackage; _asset = localPackage;
ProcessExePath = processPath; _processPath = processPath;
Log = logger ?? new NullVelopackLogger(); Log = logger ?? new NullVelopackLogger();
} }
public string GetCurrentProcessPath()
{
return _processPath ?? throw new NotSupportedException("GetCurrentProcessPath is not supported in this test implementation.");
}
public uint GetCurrentProcessId()
{
return 0; // Not implemented in this test mock
}
public void StartProcess(string exePath, IEnumerable<string> args, string workDir, bool showWindow)
{
new DefaultProcessImpl(Log).StartProcess(exePath, args, workDir, showWindow);
}
} }
} }

View File

@@ -40,21 +40,16 @@ namespace Velopack.Locators
} }
/// <summary> Create a new default locator based on the current operating system. </summary> /// <summary> Create a new default locator based on the current operating system. </summary>
public static IVelopackLocator CreateDefaultForPlatform(IVelopackLogger? logger = null) public static IVelopackLocator CreateDefaultForPlatform(IProcessImpl? processImpl = null, IVelopackLogger? logger = null)
{ {
var process = Process.GetCurrentProcess();
var processExePath = process.MainModule?.FileName
?? throw new InvalidOperationException($"Could not determine process path, please construct {nameof(IVelopackLocator)} manually.");
var processId = (uint) process.Id;
if (VelopackRuntimeInfo.IsWindows) if (VelopackRuntimeInfo.IsWindows)
return _current = new WindowsVelopackLocator(processExePath, processId, logger); return _current = new WindowsVelopackLocator(processImpl, logger);
if (VelopackRuntimeInfo.IsOSX) if (VelopackRuntimeInfo.IsOSX)
return _current = new OsxVelopackLocator(processExePath, processId, logger); return _current = new OsxVelopackLocator(processImpl, logger);
if (VelopackRuntimeInfo.IsLinux) if (VelopackRuntimeInfo.IsLinux)
return _current = new LinuxVelopackLocator(processExePath, processId, logger); return _current = new LinuxVelopackLocator(processImpl, logger);
throw new PlatformNotSupportedException($"OS platform '{VelopackRuntimeInfo.SystemOs.GetOsLongName()}' is not supported."); throw new PlatformNotSupportedException($"OS platform '{VelopackRuntimeInfo.SystemOs.GetOsLongName()}' is not supported.");
} }
@@ -64,9 +59,9 @@ namespace Velopack.Locators
_current = locator; _current = locator;
} }
internal static IVelopackLocator GetCurrentOrCreateDefault(IVelopackLogger? logger = null) internal static IVelopackLocator GetCurrentOrCreateDefault(IProcessImpl? processImpl = null, IVelopackLogger? logger = null)
{ {
_current ??= CreateDefaultForPlatform(logger); _current ??= CreateDefaultForPlatform(processImpl, logger);
return _current; return _current;
} }
@@ -94,20 +89,17 @@ namespace Velopack.Locators
/// <inheritdoc/> /// <inheritdoc/>
public abstract IVelopackLogger Log { get; } public abstract IVelopackLogger Log { get; }
/// <inheritdoc/>
public abstract uint ProcessId { get; }
/// <inheritdoc/>
public abstract string ProcessExePath { get; }
/// <inheritdoc/> /// <inheritdoc/>
public virtual bool IsPortable => false; public virtual bool IsPortable => false;
/// <inheritdoc/>
public abstract IProcessImpl Process { get; }
/// <inheritdoc/> /// <inheritdoc/>
public virtual string? ThisExeRelativePath { public virtual string? ThisExeRelativePath {
get { get {
if (AppContentDir == null) return null; if (AppContentDir == null) return null;
var path = ProcessExePath; var path = Process.GetCurrentProcessPath();
if (path.StartsWith(AppContentDir, StringComparison.OrdinalIgnoreCase)) { if (path.StartsWith(AppContentDir, StringComparison.OrdinalIgnoreCase)) {
return path.Substring(AppContentDir.Length + 1); return path.Substring(AppContentDir.Length + 1);
} else { } else {

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using NuGet.Versioning; using NuGet.Versioning;
using Velopack.Logging; using Velopack.Logging;
@@ -30,8 +32,10 @@ namespace Velopack.Locators
/// <inheritdoc /> /// <inheritdoc />
public override SemanticVersion? CurrentlyInstalledVersion { get; } public override SemanticVersion? CurrentlyInstalledVersion { get; }
private readonly Lazy<string?> _packagesDir;
/// <inheritdoc /> /// <inheritdoc />
public override string? PackagesDir => CreateSubDirIfDoesNotExist(RootAppDir, "packages"); public override string? PackagesDir => _packagesDir.Value;
/// <inheritdoc /> /// <inheritdoc />
public override IVelopackLogger Log { get; } public override IVelopackLogger Log { get; }
@@ -39,30 +43,30 @@ namespace Velopack.Locators
/// <inheritdoc /> /// <inheritdoc />
public override bool IsPortable => RootAppDir != null && File.Exists(Path.Combine(RootAppDir, ".portable")); public override bool IsPortable => RootAppDir != null && File.Exists(Path.Combine(RootAppDir, ".portable"));
/// <inheritdoc />
public override IProcessImpl Process { get; }
/// <inheritdoc /> /// <inheritdoc />
public override string? Channel { get; } public override string? Channel { get; }
/// <inheritdoc />
public override uint ProcessId { get; }
/// <inheritdoc />
public override string ProcessExePath { get; }
/// <inheritdoc cref="WindowsVelopackLocator" /> /// <inheritdoc cref="WindowsVelopackLocator" />
public WindowsVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? customLog) public WindowsVelopackLocator(IProcessImpl? processImpl, IVelopackLogger? customLog)
{ {
if (!VelopackRuntimeInfo.IsWindows) if (!VelopackRuntimeInfo.IsWindows)
throw new NotSupportedException($"Cannot instantiate {nameof(WindowsVelopackLocator)} on a non-Windows system."); throw new NotSupportedException($"Cannot instantiate {nameof(WindowsVelopackLocator)} on a non-Windows system.");
ProcessId = currentProcessId; _packagesDir = new(GetPackagesDir);
var ourPath = ProcessExePath = currentProcessPath;
var combinedLog = new CombinedVelopackLogger(); var combinedLog = new CombinedVelopackLogger();
combinedLog.Add(customLog); combinedLog.Add(customLog);
Log = combinedLog; Log = combinedLog;
Process = processImpl ??= new DefaultProcessImpl(combinedLog);
var ourPath = processImpl.GetCurrentProcessPath();
var currentProcessId = processImpl.GetCurrentProcessId();
using var initLog = new CachedVelopackLogger(combinedLog); using var initLog = new CachedVelopackLogger(combinedLog);
initLog.Info($"Initialising {nameof(WindowsVelopackLocator)}"); initLog.Info($"Initializing {nameof(WindowsVelopackLocator)}");
// We try various approaches here. Firstly, if Update.exe is in the parent directory, // We try various approaches here. Firstly, if Update.exe is in the parent directory,
// we use that. If it's not present, we search for a parent "current" or "app-{ver}" directory, // we use that. If it's not present, we search for a parent "current" or "app-{ver}" directory,
@@ -70,7 +74,6 @@ namespace Velopack.Locators
// There is some legacy code here, because it's possible that we're running in an "app-{ver}" // There is some legacy code here, because it's possible that we're running in an "app-{ver}"
// directory which is NOT containing a sq.version, in which case we need to infer a lot of info. // directory which is NOT containing a sq.version, in which case we need to infer a lot of info.
ProcessExePath = Path.GetFullPath(ourPath);
string myDirPath = Path.GetDirectoryName(ourPath)!; string myDirPath = Path.GetDirectoryName(ourPath)!;
var myDirName = Path.GetFileName(myDirPath); var myDirName = Path.GetFileName(myDirPath);
var possibleUpdateExe = Path.GetFullPath(Path.Combine(myDirPath, "..", "Update.exe")); var possibleUpdateExe = Path.GetFullPath(Path.Combine(myDirPath, "..", "Update.exe"));
@@ -120,36 +123,99 @@ namespace Velopack.Locators
} }
} }
bool fileLogCreated = false; if (UpdateExePath != null
&& Path.GetDirectoryName(UpdateExePath) is { } updateExeDirectory
&& !PathUtil.IsDirectoryWritable(updateExeDirectory)) {
var tempTargetUpdateExe = Path.Combine(TempAppRootDirectory, "Update.exe");
if (File.Exists(UpdateExePath) && !File.Exists(tempTargetUpdateExe)) {
initLog.Warn("Application directory is not writable. Copying Update.exe to temp location: " + tempTargetUpdateExe);
Debugger.Launch();
Directory.CreateDirectory(TempAppRootDirectory);
File.Copy(UpdateExePath, tempTargetUpdateExe);
}
UpdateExePath = tempTargetUpdateExe;
}
//bool fileLogCreated = false;
Exception? fileLogException = null;
if (!String.IsNullOrEmpty(AppId) && !String.IsNullOrEmpty(RootAppDir)) { if (!String.IsNullOrEmpty(AppId) && !String.IsNullOrEmpty(RootAppDir)) {
try { try {
var logFilePath = Path.Combine(RootAppDir, DefaultLoggingFileName); var logFilePath = Path.Combine(RootAppDir, DefaultLoggingFileName);
var fileLog = new FileVelopackLogger(logFilePath, currentProcessId); var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
combinedLog.Add(fileLog); combinedLog.Add(fileLog);
fileLogCreated = true; //fileLogCreated = true;
} catch (Exception ex2) { } catch (Exception ex) {
initLog.Error("Unable to create default file logger: " + ex2); fileLogException = ex;
} }
} }
// if the RootAppDir was unwritable, or we don't know the app id, we could try to write to the temp folder instead. // if the RootAppDir was unwritable, or we don't know the app id, we could try to write to the temp folder instead.
if (!fileLogCreated) { Exception? tempFileLogException = null;
if (fileLogException is not null) {
try { try {
var logFileName = String.IsNullOrEmpty(AppId) ? DefaultLoggingFileName : $"velopack_{AppId}.log"; var logFileName = String.IsNullOrEmpty(AppId) ? DefaultLoggingFileName : $"velopack_{AppId}.log";
var logFilePath = Path.Combine(Path.GetTempPath(), logFileName); var logFilePath = Path.Combine(Path.GetTempPath(), logFileName);
var fileLog = new FileVelopackLogger(logFilePath, currentProcessId); var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
combinedLog.Add(fileLog); combinedLog.Add(fileLog);
} catch (Exception ex2) { } catch (Exception ex) {
initLog.Error("Unable to create temp folder file logger: " + ex2); tempFileLogException = ex;
} }
} }
if (tempFileLogException is not null) {
//NB: fileLogException is not null here
initLog.Error("Unable to create file logger: " + new AggregateException(fileLogException!, tempFileLogException));
} else if (fileLogException is not null) {
initLog.Info("Unable to create file logger; using temp directory for log instead");
initLog.Trace($"File logger exception: {fileLogException}");
}
if (AppId == null) { if (AppId == null) {
initLog.Warn( initLog.Warn(
$"Failed to initialise {nameof(WindowsVelopackLocator)}. This could be because the program is not installed or packaged properly."); $"Failed to initialize {nameof(WindowsVelopackLocator)}. This could be because the program is not installed or packaged properly.");
} else { } else {
initLog.Info($"Initialised {nameof(WindowsVelopackLocator)} for {AppId} v{CurrentlyInstalledVersion}"); initLog.Info($"Initialized {nameof(WindowsVelopackLocator)} for {AppId} v{CurrentlyInstalledVersion}");
} }
} }
private string? GetPackagesDir()
{
const string PackagesDirName = "packages";
string? writableRootDir = PossibleDirectories()
.FirstOrDefault(IsWritable);
if (writableRootDir == null) {
Log.Warn("Unable to find a writable root directory for package.");
return null;
}
Log.Trace("Using writable root directory: " + writableRootDir);
return CreateSubDirIfDoesNotExist(writableRootDir, PackagesDirName);
static bool IsWritable(string? directoryPath)
{
if (directoryPath == null) return false;
try {
if (!Directory.Exists(directoryPath)) {
Directory.CreateDirectory(directoryPath);
}
return PathUtil.IsDirectoryWritable(directoryPath);
} catch {
return false;
}
}
IEnumerable<string?> PossibleDirectories()
{
yield return RootAppDir;
yield return TempAppRootDirectory;
}
}
private string TempAppRootDirectory => Path.Combine(Path.GetTempPath(), "velopack_" + AppId);
} }
} }

View File

@@ -1,11 +1,5 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Velopack.Locators; using Velopack.Locators;
using Velopack.Logging; using Velopack.Logging;
using Velopack.Util; using Velopack.Util;
@@ -19,40 +13,6 @@ namespace Velopack
/// </summary> /// </summary>
public static class UpdateExe public static class UpdateExe
{ {
[DllImport("user32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AllowSetForegroundWindow(int dwProcessId);
private static Process StartUpdateExe(IVelopackLogger logger, IVelopackLocator locator, IEnumerable<string> args)
{
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = locator.UpdateExePath!,
WorkingDirectory = Path.GetDirectoryName(locator.UpdateExePath)!,
};
psi.AppendArgumentListSafe(args, out var debugArgs);
logger.Debug($"Running: {psi.FileName} {debugArgs}");
var p = Process.Start(psi);
if (p == null) {
throw new Exception("Failed to launch Update.exe process.");
}
if (VelopackRuntimeInfo.IsWindows) {
try {
// this is an attempt to work around a bug where the restarted app fails to come to foreground.
if (!AllowSetForegroundWindow(p.Id))
throw new Win32Exception();
} catch (Exception ex) {
logger.LogWarning(ex, "Failed to allow Update.exe to set foreground window.");
}
}
logger.Info("Update.exe executed successfully.");
return p;
}
/// <summary> /// <summary>
/// Runs Update.exe in the current working directory with the 'start' command which will simply start the application. /// Runs Update.exe in the current working directory with the 'start' command which will simply start the application.
/// Combined with the `waitForExit` parameter, this can be used to gracefully restart the application. /// Combined with the `waitForExit` parameter, this can be used to gracefully restart the application.
@@ -79,10 +39,24 @@ namespace Velopack
} }
} }
StartUpdateExe(locator.Log, locator, args); var updatePath = locator.UpdateExePath!;
var workingDir = Path.GetDirectoryName(updatePath)!;
locator.Process.StartProcess(updatePath, args, workingDir, false);
locator.Log.Info("Update.exe [start] executed successfully.");
} }
private static Process ApplyImpl(IVelopackLocator? locator, VelopackAsset? toApply, bool silent, uint waitPid, bool restart, /// <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="toApply">The update package you wish to apply, can be left null.</param>
/// <param name="waitPid">Optionally wait for the specified process to exit before continuing.</param>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
public static void Apply(IVelopackLocator? locator, VelopackAsset? toApply, bool silent, uint waitPid, bool restart,
string[]? restartArgs = null) string[]? restartArgs = null)
{ {
locator ??= VelopackLocator.Current; locator ??= VelopackLocator.Current;
@@ -107,6 +81,9 @@ namespace Velopack
if (!restart) args.Add("--norestart"); // restarting is now the default Update.exe behavior if (!restart) args.Add("--norestart"); // restarting is now the default Update.exe behavior
args.Add("--root");
args.Add(locator.RootAppDir!);
if (restart && restartArgs != null && restartArgs.Length > 0) { if (restart && restartArgs != null && restartArgs.Length > 0) {
args.Add("--"); args.Add("--");
foreach (var a in restartArgs) { foreach (var a in restartArgs) {
@@ -114,42 +91,10 @@ namespace Velopack
} }
} }
return StartUpdateExe(locator.Log, locator, args); var updatePath = locator.UpdateExePath!;
} var workingDir = Path.GetDirectoryName(updatePath)!;
locator.Process.StartProcess(updatePath, args, workingDir, false);
/// <summary> locator.Log.Info("Update.exe [apply] executed successfully.");
/// 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="toApply">The update package you wish to apply, can be left null.</param>
/// <param name="waitPid">Optionally wait for the specified process to exit before continuing.</param>
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
/// <exception cref="Exception">Thrown if Update.exe does not initialize properly.</exception>
public static void Apply(IVelopackLocator? locator, VelopackAsset? toApply, bool silent, uint waitPid, bool restart,
string[]? restartArgs = null)
{
var process = ApplyImpl(locator, toApply, silent, waitPid, restart, restartArgs);
Thread.Sleep(500);
if (process.HasExited) {
throw new Exception($"Update.exe process exited too soon ({process.ExitCode}).");
}
}
/// <inheritdoc cref="Apply"/>
public static async Task ApplyAsync(IVelopackLocator? locator, VelopackAsset? toApply, bool silent, uint waitPid, bool restart,
string[]? restartArgs = null)
{
var process = ApplyImpl(locator, toApply, silent, waitPid, restart, restartArgs);
await Task.Delay(500).ConfigureAwait(false);
if (process.HasExited) {
throw new Exception($"Update.exe process exited too soon ({process.ExitCode}).");
}
} }
} }
} }

View File

@@ -44,13 +44,8 @@ namespace Velopack
/// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param> /// <param name="restartArgs">The arguments to pass to the application when it is restarted.</param>
public void WaitExitThenApplyUpdates(VelopackAsset? toApply, bool silent = false, bool restart = true, string[]? restartArgs = null) public void WaitExitThenApplyUpdates(VelopackAsset? toApply, bool silent = false, bool restart = true, string[]? restartArgs = null)
{ {
UpdateExe.Apply(Locator, toApply, silent, Locator.ProcessId, restart, restartArgs); UpdateExe.Apply(Locator, toApply, silent, Locator.Process.GetCurrentProcessId(), restart, restartArgs);
}
/// <inheritdoc cref="WaitExitThenApplyUpdates"/>
public async Task WaitExitThenApplyUpdatesAsync(VelopackAsset? toApply, bool silent = false, bool restart = true, string[]? restartArgs = null)
{
await UpdateExe.ApplyAsync(Locator, toApply, silent, Locator.ProcessId, restart, restartArgs).ConfigureAwait(false);
} }
} }
} }

View File

@@ -172,7 +172,7 @@ namespace Velopack
VelopackLocator.SetCurrentLocator(_customLocator); VelopackLocator.SetCurrentLocator(_customLocator);
} }
var locator = VelopackLocator.GetCurrentOrCreateDefault(_customLogger); var locator = VelopackLocator.GetCurrentOrCreateDefault(null, _customLogger);
var log = locator.Log; var log = locator.Log;
log.Info($"Starting VelopackApp.Run (library version {VelopackRuntimeInfo.VelopackNugetVersion})."); log.Info($"Starting VelopackApp.Run (library version {VelopackRuntimeInfo.VelopackNugetVersion}).");
@@ -231,7 +231,7 @@ namespace Velopack
log.Info($"Launching app is out-dated. Current: {myVersion}, Newest Local Available: {latestLocal.Version}"); log.Info($"Launching app is out-dated. Current: {myVersion}, Newest Local Available: {latestLocal.Version}");
if (!restarted && _autoApply) { if (!restarted && _autoApply) {
log.Info("Auto apply is true, so restarting to apply update..."); log.Info("Auto apply is true, so restarting to apply update...");
UpdateExe.Apply(locator, latestLocal, false, locator.ProcessId, true, args); UpdateExe.Apply(locator, latestLocal, false, locator.Process.GetCurrentProcessId(), true, args);
Exit(0); Exit(0);
} else { } else {
log.Info("Pre-condition failed, we will not restart to apply updates. (restarted: " + restarted + ", autoApply: " + _autoApply + ")"); log.Info("Pre-condition failed, we will not restart to apply updates. (restarted: " + restarted + ", autoApply: " + _autoApply + ")");