C# library writes to file + custom location at same time

This commit is contained in:
Caelan Sayler
2025-03-12 21:57:44 +00:00
committed by Caelan
parent b0707b124b
commit 4f10c94565
8 changed files with 257 additions and 89 deletions

View File

@@ -11,37 +11,37 @@ namespace Velopack.Locators
public interface IVelopackLocator
{
/// <summary> The unique application Id. This is used in various app paths. </summary>
public string? AppId { get; }
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; }
string? RootAppDir { get; }
/// <summary> The directory in which nupkg files are stored for this application. </summary>
public string? PackagesDir { get; }
string? PackagesDir { get; }
/// <summary> The directory in which versioned application files are stored. </summary>
public string? AppContentDir { get; }
string? AppContentDir { get; }
/// <summary> The temporary directory for this application. </summary>
public string? AppTempDir { get; }
string? AppTempDir { get; }
/// <summary> The path to the current Update.exe or similar on other operating systems. </summary>
public string? UpdateExePath { get; }
string? UpdateExePath { get; }
/// <summary> The currently installed version of the application, or null if the app is not installed. </summary>
public SemanticVersion? CurrentlyInstalledVersion { get; }
SemanticVersion? CurrentlyInstalledVersion { get; }
/// <summary> The path from <see cref="AppContentDir"/> to this executable. </summary>
public string? ThisExeRelativePath { get; }
string? ThisExeRelativePath { get; }
/// <summary> The release channel this package was built for. </summary>
public string? Channel { get; }
string? Channel { get; }
/// <summary> The logging interface to use for Velopack diagnostic messages. </summary>
public IVelopackLogger Log { get; }
IVelopackLogger Log { get; }
/// <summary>
/// A flag indicating if this is a portable build, and that the settings should be self-contained in the package.
@@ -49,34 +49,34 @@ namespace Velopack.Locators
/// On OSX and Linux, this is always false, because settings and application files should be stored in the user's
/// home directory.
/// </summary>
public bool IsPortable { get; }
bool IsPortable { get; }
/// <summary>
/// The process for which the Velopack Locator has been constructed. This should usually be the current process path.
/// </summary>
public string ProcessExePath { get; }
string ProcessExePath { 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>
public uint ProcessId { get; }
uint ProcessId { get; }
/// <summary>
/// Finds .nupkg files in the PackagesDir and returns a list of ReleaseEntryName objects.
/// </summary>
public List<VelopackAsset> GetLocalPackages();
List<VelopackAsset> GetLocalPackages();
/// <summary>
/// Finds latest .nupkg file in the PackagesDir or null if not found.
/// </summary>
public VelopackAsset? GetLatestLocalFullPackage();
VelopackAsset? GetLatestLocalFullPackage();
/// <summary>
/// Unique identifier for this user which is used to calculate whether this user is eligible for
/// staged roll outs.
/// </summary>
public Guid? GetOrCreateStagedUserId();
Guid? GetOrCreateStagedUserId();
}
}

View File

@@ -32,7 +32,7 @@ namespace Velopack.Locators
/// <inheritdoc />
public override string? Channel { get; }
/// <inheritdoc />
public override IVelopackLogger Log { get; }
@@ -50,10 +50,10 @@ namespace Velopack.Locators
/// <summary> File path of the .AppImage which mounted and ran this application. </summary>
public string? AppImagePath => Environment.GetEnvironmentVariable("APPIMAGE");
/// <inheritdoc />
public override uint ProcessId { get; }
/// <inheritdoc />
public override string ProcessExePath { get; }
@@ -61,46 +61,63 @@ namespace Velopack.Locators
/// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the
/// app information from metadata embedded in the .app.
/// </summary>
public LinuxVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? logger)
public LinuxVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? customLog)
{
if (!VelopackRuntimeInfo.IsLinux)
throw new NotSupportedException("Cannot instantiate LinuxVelopackLocator on a non-linux system.");
throw new NotSupportedException($"Cannot instantiate {nameof(LinuxVelopackLocator)} on a non-linux system.");
ProcessId = currentProcessId;
var ourPath = ProcessExePath = currentProcessPath;
logger ??= new FileVelopackLogger("/tmp/velopack.log", currentProcessId);
logger.Info($"Initialising {nameof(LinuxVelopackLocator)}");
Log = logger;
var combinedLog = new CombinedVelopackLogger();
combinedLog.Add(customLog);
Log = combinedLog;
using var initLog = new CachedVelopackLogger(combinedLog);
initLog.Info($"Initialising {nameof(LinuxVelopackLocator)}");
var logFilePath = Path.Combine(Path.GetTempPath(), DefaultLoggingFileName);
// are we inside a mounted .AppImage?
var ix = ourPath.IndexOf("/usr/bin/", StringComparison.InvariantCultureIgnoreCase);
if (ix <= 0) {
logger.Warn(
$"Unable to locate .AppImage root from '{ourPath}'. " +
$"This warning indicates that the application is not running from a mounted .AppImage, for example during development.");
return;
}
if (ix > 0) {
var rootDir = ourPath.Substring(0, ix);
var contentsDir = Path.Combine(rootDir, "usr", "bin");
var updateExe = Path.Combine(contentsDir, "UpdateNix");
var metadataPath = Path.Combine(contentsDir, CoreUtil.SpecVersionFileName);
var rootDir = ourPath.Substring(0, ix);
var contentsDir = Path.Combine(rootDir, "usr", "bin");
var updateExe = Path.Combine(contentsDir, "UpdateNix");
var metadataPath = Path.Combine(contentsDir, CoreUtil.SpecVersionFileName);
if (!String.IsNullOrEmpty(AppImagePath) && File.Exists(AppImagePath)) {
if (File.Exists(updateExe) && PackageManifest.TryParseFromFile(metadataPath, out var manifest)) {
logger.Info("Located valid manifest file at: " + metadataPath);
AppId = manifest.Id;
RootAppDir = rootDir;
AppContentDir = contentsDir;
UpdateExePath = updateExe;
CurrentlyInstalledVersion = manifest.Version;
Channel = manifest.Channel;
if (!String.IsNullOrEmpty(AppImagePath) && File.Exists(AppImagePath)) {
if (File.Exists(updateExe) && PackageManifest.TryParseFromFile(metadataPath, out var manifest)) {
initLog.Info("Located valid manifest file at: " + metadataPath);
AppId = manifest.Id;
RootAppDir = rootDir;
AppContentDir = contentsDir;
UpdateExePath = updateExe;
CurrentlyInstalledVersion = manifest.Version;
Channel = manifest.Channel;
logFilePath = Path.Combine(Path.GetTempPath(), $"velopack_{manifest.Id}.log");
} else {
initLog.Error("Unable to locate UpdateNix in " + contentsDir);
}
} else {
logger.Error("Unable to locate UpdateNix in " + contentsDir);
initLog.Error("Unable to locate .AppImage ($APPIMAGE)");
}
} else {
logger.Error("Unable to locate .AppImage ($APPIMAGE)");
initLog.Warn(
$"Unable to locate .AppImage root from '{ourPath}'. " +
$"This warning indicates that the application is not running from a mounted .AppImage, for example during development.");
}
try {
var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
combinedLog.Add(fileLog);
} catch (Exception ex) {
initLog.Error("Unable to create file logger: " + ex);
}
if (AppId == null) {
initLog.Warn($"Failed to initialise {nameof(LinuxVelopackLocator)}. This could be because the program is not in an .AppImage.");
} else {
initLog.Info($"Initialised {nameof(LinuxVelopackLocator)} for {AppId} v{CurrentlyInstalledVersion}");
}
}
}

View File

@@ -47,10 +47,10 @@ namespace Velopack.Locators
/// <inheritdoc />
public override string? Channel { get; }
/// <inheritdoc />
public override uint ProcessId { get; }
/// <inheritdoc />
public override string ProcessExePath { get; }
@@ -58,40 +58,66 @@ namespace Velopack.Locators
/// Creates a new <see cref="OsxVelopackLocator"/> and auto-detects the
/// app information from metadata embedded in the .app.
/// </summary>
public OsxVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? logger)
public OsxVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? customLog)
{
if (!VelopackRuntimeInfo.IsOSX)
throw new NotSupportedException("Cannot instantiate OsxLocator on a non-osx system.");
throw new NotSupportedException($"Cannot instantiate {nameof(OsxVelopackLocator)} on a non-osx system.");
ProcessId = currentProcessId;
var ourPath = ProcessExePath = currentProcessPath;
var userLogDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Logs");
var logPath = Directory.Exists(userLogDir) ? Path.Combine(userLogDir, "velopack.log") : "/tmp/velopack.log";
logger ??= new FileVelopackLogger(logPath, currentProcessId);
logger.Info($"Initialising {nameof(OsxVelopackLocator)}");
Log = logger;
var combinedLog = new CombinedVelopackLogger();
combinedLog.Add(customLog);
Log = combinedLog;
using var initLog = new CachedVelopackLogger(combinedLog);
initLog.Info($"Initialising {nameof(OsxVelopackLocator)}");
string logFolder = Path.GetTempPath();
if (!string.IsNullOrEmpty(HomeDir) && Directory.Exists(HomeDir)) {
var userLogsFolder = Path.Combine(HomeDir!, "Library", "Logs");
if (!Directory.Exists(userLogsFolder)) {
logFolder = userLogsFolder;
}
}
var logFileName = DefaultLoggingFileName;
// are we inside a .app?
var ix = ourPath.IndexOf(".app/", StringComparison.InvariantCultureIgnoreCase);
if (ix <= 0) {
logger.Warn($"Unable to locate .app root from '{ourPath}'");
return;
if (ix > 0) {
var appPath = ourPath.Substring(0, ix + 4);
var contentsDir = Path.Combine(appPath, "Contents");
var macosDir = Path.Combine(contentsDir, "MacOS");
var updateExe = Path.Combine(macosDir, "UpdateMac");
var metadataPath = Path.Combine(macosDir, CoreUtil.SpecVersionFileName);
if (File.Exists(updateExe) && PackageManifest.TryParseFromFile(metadataPath, out var manifest)) {
initLog.Info("Located valid manifest file at: " + metadataPath);
AppId = manifest.Id;
RootAppDir = appPath;
UpdateExePath = updateExe;
CurrentlyInstalledVersion = manifest.Version;
Channel = manifest.Channel;
logFileName = $"velopack_{manifest.Id}.log";
}
} else {
initLog.Warn($"Unable to locate .app root from '{ourPath}'");
}
var appPath = ourPath.Substring(0, ix + 4);
var contentsDir = Path.Combine(appPath, "Contents");
var macosDir = Path.Combine(contentsDir, "MacOS");
var updateExe = Path.Combine(macosDir, "UpdateMac");
var metadataPath = Path.Combine(macosDir, CoreUtil.SpecVersionFileName);
try {
var logFilePath = Path.Combine(logFolder, logFileName);
var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
combinedLog.Add(fileLog);
} catch (Exception ex) {
initLog.Error("Unable to create file logger: " + ex);
}
if (File.Exists(updateExe) && PackageManifest.TryParseFromFile(metadataPath, out var manifest)) {
logger.Info("Located valid manifest file at: " + metadataPath);
AppId = manifest.Id;
RootAppDir = appPath;
UpdateExePath = updateExe;
CurrentlyInstalledVersion = manifest.Version;
Channel = manifest.Channel;
if (AppId == null) {
initLog.Warn($"Failed to initialise {nameof(OsxVelopackLocator)}. This could be because the program is not in a .app bundle.");
} else {
initLog.Info($"Initialised {nameof(OsxVelopackLocator)} for {AppId} v{CurrentlyInstalledVersion}");
}
}
}

View File

@@ -15,6 +15,11 @@ namespace Velopack.Locators
public abstract class VelopackLocator : IVelopackLocator
{
private static IVelopackLocator? _current;
/// <summary>
/// The default log file name for Velopack.
/// </summary>
protected const string DefaultLoggingFileName = "velopack.log";
/// <summary>
/// Check if a VelopackLocator has been set for the current process.

View File

@@ -49,14 +49,21 @@ namespace Velopack.Locators
public override string ProcessExePath { get; }
/// <inheritdoc cref="WindowsVelopackLocator" />
public WindowsVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? logger)
public WindowsVelopackLocator(string currentProcessPath, uint currentProcessId, IVelopackLogger? customLog)
{
if (!VelopackRuntimeInfo.IsWindows)
throw new NotSupportedException("Cannot instantiate WindowsLocator on a non-Windows system.");
throw new NotSupportedException($"Cannot instantiate {nameof(WindowsVelopackLocator)} on a non-Windows system.");
ProcessId = currentProcessId;
var ourPath = ProcessExePath = currentProcessPath;
var combinedLog = new CombinedVelopackLogger();
combinedLog.Add(customLog);
Log = combinedLog;
using var initLog = new CachedVelopackLogger(combinedLog);
initLog.Info($"Initialising {nameof(WindowsVelopackLocator)}");
// 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,
// which could designate that this executable is running in a nested sub-directory.
@@ -73,11 +80,9 @@ namespace Velopack.Locators
// we're running in a directory with an Update.exe in the parent directory
var manifestFile = Path.Combine(myDirPath, CoreUtil.SpecVersionFileName);
var rootDir = Path.GetDirectoryName(possibleUpdateExe)!;
logger ??= new FileVelopackLogger(Path.Combine(rootDir, "velopack.log"), currentProcessId);
Log = logger;
if (PackageManifest.TryParseFromFile(manifestFile, out var manifest)) {
// ideal, the info we need is in a manifest file.
logger.Info($"{nameof(WindowsVelopackLocator)}: Update.exe in parent dir, Located valid manifest file at: " + manifestFile);
initLog.Info($"{nameof(WindowsVelopackLocator)}: Update.exe in parent dir, Located valid manifest file at: " + manifestFile);
AppId = manifest.Id;
CurrentlyInstalledVersion = manifest.Version;
RootAppDir = rootDir;
@@ -86,14 +91,15 @@ namespace Velopack.Locators
Channel = manifest.Channel;
} else if (PathUtil.PathPartStartsWith(myDirName, "app-") && NuGetVersion.TryParse(myDirName.Substring(4), out var version)) {
// this is a legacy case, where we're running in an 'root/app-*/' directory, and there is no manifest.
logger.Warn("Update.exe in parent dir, Legacy app-* directory detected, sq.version not found. Using directory name for AppId and Version.");
initLog.Warn(
"Update.exe in parent dir, Legacy app-* directory detected, sq.version not found. Using directory name for AppId and Version.");
AppId = Path.GetFileName(Path.GetDirectoryName(possibleUpdateExe));
CurrentlyInstalledVersion = version;
RootAppDir = rootDir;
UpdateExePath = possibleUpdateExe;
AppContentDir = myDirPath;
} else {
logger.Error("Update.exe in parent dir, but unable to locate a valid manifest file at: " + manifestFile);
initLog.Error("Update.exe in parent dir, but unable to locate a valid manifest file at: " + manifestFile);
}
} else if (ixCurrent > 0) {
// this is an attempt to handle the case where we are running in a nested current directory.
@@ -103,10 +109,8 @@ namespace Velopack.Locators
possibleUpdateExe = Path.GetFullPath(Path.Combine(rootDir, "Update.exe"));
// we only support parsing a manifest when we're in a nested current directory. no legacy fallback.
if (File.Exists(possibleUpdateExe) && PackageManifest.TryParseFromFile(manifestFile, out var manifest)) {
logger ??= new FileVelopackLogger(Path.Combine(rootDir, "velopack.log"), currentProcessId);
Log = logger;
logger.Warn("Running in deeply nested directory. This is not an advised use-case.");
logger.Info("Located valid manifest file at: " + manifestFile);
initLog.Warn("Running in deeply nested directory. This is not an advised use-case.");
initLog.Info("Located valid manifest file at: " + manifestFile);
RootAppDir = Path.GetDirectoryName(possibleUpdateExe);
UpdateExePath = possibleUpdateExe;
AppId = manifest.Id;
@@ -116,14 +120,36 @@ namespace Velopack.Locators
}
}
if (Log == null) {
bool fileLogCreated = false;
if (!String.IsNullOrEmpty(AppId) && !String.IsNullOrEmpty(RootAppDir)) {
try {
Log = new FileVelopackLogger(Path.Combine(AppContext.BaseDirectory, "velopack.log"), currentProcessId);
} catch (Exception ex) {
Debug.WriteLine("Error creating Velopack logger: " + ex);
Log = new NullVelopackLogger();
var logFilePath = Path.Combine(RootAppDir, DefaultLoggingFileName);
var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
combinedLog.Add(fileLog);
fileLogCreated = true;
} catch (Exception ex2) {
initLog.Error("Unable to create default file logger: " + ex2);
}
}
// 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) {
try {
var logFileName = String.IsNullOrEmpty(AppId) ? DefaultLoggingFileName : $"velopack_{AppId}.log";
var logFilePath = Path.Combine(Path.GetTempPath(), logFileName);
var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
combinedLog.Add(fileLog);
} catch (Exception ex2) {
initLog.Error("Unable to create temp folder file logger: " + ex2);
}
}
if (AppId == null) {
initLog.Warn(
$"Failed to initialise {nameof(WindowsVelopackLocator)}. This could be because the program is not installed or packaged properly.");
} else {
initLog.Info($"Initialised {nameof(WindowsVelopackLocator)} for {AppId} v{CurrentlyInstalledVersion}");
}
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace Velopack.Logging
{
internal class CachedVelopackLogger : IVelopackLogger, IDisposable
{
private readonly List<(VelopackLogLevel logLevel, string? message, Exception? exception)> _cache = new();
private readonly IVelopackLogger _logger;
private readonly object _lock = new();
private bool _committed;
public CachedVelopackLogger(IVelopackLogger logger)
{
_logger = logger;
}
public void Log(VelopackLogLevel logLevel, string? message, Exception? exception)
{
lock (_lock) {
if (_committed) {
_logger.Log(logLevel, message, exception);
} else {
_cache.Add((logLevel, message, exception));
}
}
}
private void Commit()
{
lock (_lock) {
if (_committed) {
return;
}
foreach (var (logLevel, message, exception) in _cache) {
_logger.Log(logLevel, message, exception);
}
_cache.Clear();
_committed = true;
}
}
public void Dispose()
{
Commit();
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Velopack.Logging
{
internal class CombinedVelopackLogger : IVelopackLogger, IDisposable
{
private readonly List<IVelopackLogger> _loggers = new();
public void Log(VelopackLogLevel logLevel, string? message, Exception? exception)
{
foreach (var logger in _loggers) {
try {
logger.Log(logLevel, message, exception);
} catch (Exception ex) {
Debug.WriteLine($"Error logging to {logger.GetType().Name} ({ex}) {Environment.NewLine} [{logLevel}] {message}");
}
}
}
public void Add(IVelopackLogger? logger)
{
if (logger != null) {
_loggers.Add(logger);
}
}
public void Dispose()
{
var localLoggers = _loggers.ToArray();
_loggers.Clear();
foreach (var logger in localLoggers) {
try {
if (logger is IDisposable disposable)
disposable.Dispose();
} catch (Exception ex) {
Debug.WriteLine($"Error disposing {logger.GetType().Name} ({ex})");
}
}
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Velopack.Logging
{
public class FileVelopackLogger : IVelopackLogger, IDisposable
{
public uint ProcessId { get; }
private uint ProcessId { get; }
private readonly object _lock = new();
private readonly StreamWriter _writer;
private readonly FileStream _fileStream;