Merge pull request #1753 from rmcrackan/rmcrackan/1748-upgrader

In-app upgrades now wait for the real installer and treat failures as failures
This commit is contained in:
rmcrackan
2026-04-20 10:21:13 -04:00
committed by GitHub
6 changed files with 108 additions and 39 deletions

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Threading.Tasks;
namespace LibationFileManager;
@@ -8,7 +9,8 @@ public interface IInteropFunctions
void SetFolderIcon(byte[] imageJpegBytes, string directory);
void DeleteFolderIcon(string directory);
Process? RunAsRoot(string exe, string args);
void InstallUpgrade(string upgradeBundle);
/// <summary>Waits for the privileged installer where possible and throws if it fails.</summary>
Task InstallUpgradeAsync(string upgradeBundle);
bool CanUpgrade { get; }
string ReleaseIdString { get; }
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace LibationFileManager;
@@ -15,5 +16,5 @@ public class NullInteropFunctions : IInteropFunctions
public bool CanUpgrade => throw new PlatformNotSupportedException();
public string ReleaseIdString => throw new PlatformNotSupportedException();
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
public void InstallUpgrade(string updateBundle) => throw new PlatformNotSupportedException();
public Task InstallUpgradeAsync(string updateBundle) => throw new PlatformNotSupportedException();
}

View File

@@ -215,10 +215,17 @@ public abstract class UpgraderBase
{
DownloadCompleted?.Invoke(this, true);
//Install the upgrade
Serilog.Log.Logger.Information($"Begin running auto-upgrader");
interop.InstallUpgrade(upgradeBundle);
Serilog.Log.Logger.Information($"Completed running auto-upgrader");
try
{
await interop.InstallUpgradeAsync(upgradeBundle);
Serilog.Log.Logger.Information($"Completed running auto-upgrader");
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Auto-upgrader did not complete successfully");
OnUpgradeFailed("The upgrade installer did not complete successfully. You can install the downloaded package manually from your temp folder.", ex);
}
}
return result;

View File

@@ -1,6 +1,8 @@
using AppScaffolding;
using LibationFileManager;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace LinuxConfigApp;
@@ -25,21 +27,89 @@ internal class LinuxInterop : IInteropFunctions
public void SetFolderIcon(byte[] imageJpegBytes, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + (File.Exists("/bin/apt") ? "_DEB" : "_RPM");
public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + (File.Exists("/usr/bin/apt") || File.Exists("/bin/apt") ? "_DEB" : "_RPM");
//only run the auto upgrader if the current app was installed from the
//.deb or .rpm package. Try to detect this by checking if the symlink exists.
public bool CanUpgrade => File.Exists("/bin/libation");
public void InstallUpgrade(string upgradeBundle)
public bool CanUpgrade => File.Exists("/usr/bin/libation") || File.Exists("/bin/libation");
public async Task InstallUpgradeAsync(string upgradeBundle)
{
if (File.Exists("/bin/dnf5"))
RunAsRoot("dnf5", $"install -y '{upgradeBundle}'");
else if (File.Exists("/bin/dnf"))
RunAsRoot("dnf", $"install -y '{upgradeBundle}'");
else if (File.Exists("/bin/yum"))
RunAsRoot("yum", $"install -y '{upgradeBundle}'");
else
RunAsRoot("apt", $"install '{upgradeBundle}'");
if (string.IsNullOrWhiteSpace(upgradeBundle) || !File.Exists(upgradeBundle))
throw new FileNotFoundException("Upgrade bundle not found.", upgradeBundle);
if (!TryResolvePackageManager(upgradeBundle, out var pkgExe, out var pkgArgs))
throw new PlatformNotSupportedException("Could not find apt, dnf, yum, or dnf5 to install the upgrade.");
if (FindPkexec(out var pkexec))
{
var psi = new ProcessStartInfo
{
FileName = pkexec,
UseShellExecute = false,
};
// pkexec requires an absolute path to the program on modern polkit.
foreach (var a in new[] { pkgExe }.Concat(pkgArgs))
psi.ArgumentList.Add(a);
if (string.Equals(Path.GetFileName(pkgExe), "apt", StringComparison.OrdinalIgnoreCase))
psi.Environment["DEBIAN_FRONTEND"] = "noninteractive";
var proc = Process.Start(psi);
if (proc is null)
throw new InvalidOperationException("Failed to start pkexec.");
await proc.WaitForExitAsync();
if (proc.ExitCode != 0)
throw new InvalidOperationException($"Package manager exited with code {proc.ExitCode}.");
return;
}
// Terminal + runasroot.sh: completion cannot be tracked reliably; user must watch the window.
var legacyArgs = string.Join(" ", pkgArgs);
Serilog.Log.Logger.Warning("pkexec not found; launching install in a terminal. Completion cannot be verified automatically.");
RunAsRoot(pkgExe, legacyArgs);
}
private static bool TryResolvePackageManager(string upgradeBundle, out string pkgExe, out string[] pkgArgs)
{
if (TryFirstExisting(out pkgExe, "/usr/bin/dnf5", "/bin/dnf5"))
{
pkgArgs = new[] { "install", "-y", upgradeBundle };
return true;
}
if (TryFirstExisting(out pkgExe, "/usr/bin/dnf", "/bin/dnf"))
{
pkgArgs = new[] { "install", "-y", upgradeBundle };
return true;
}
if (TryFirstExisting(out pkgExe, "/usr/bin/yum", "/bin/yum"))
{
pkgArgs = new[] { "install", "-y", upgradeBundle };
return true;
}
if (TryFirstExisting(out pkgExe, "/usr/bin/apt", "/bin/apt"))
{
pkgArgs = new[] { "install", "-y", "-o", "Dpkg::Options::=--force-confdef", "-o", "Dpkg::Options::=--force-confold", upgradeBundle };
return true;
}
pkgExe = "";
pkgArgs = Array.Empty<string>();
return false;
}
private static bool TryFirstExisting(out string path, params string[] candidates)
{
foreach (var c in candidates)
{
if (File.Exists(c))
{
path = c;
return true;
}
}
path = "";
return false;
}
private bool FindPkexec(out string? exePath)
@@ -49,7 +119,7 @@ internal class LinuxInterop : IInteropFunctions
exePath = "/usr/bin/pkexec";
return true;
}
else if (File.Exists("/bin/pkexec"))
if (File.Exists("/bin/pkexec"))
{
exePath = "/bin/pkexec";
return true;
@@ -60,24 +130,6 @@ internal class LinuxInterop : IInteropFunctions
public Process? RunAsRoot(string exe, string args)
{
//try to use polkit directly
if (FindPkexec(out var pkexec))
{
ProcessStartInfo psi = new()
{
FileName = pkexec,
Arguments = $"\"{exe}\" {args}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
try
{
return Process.Start(psi);
}
catch {/* fall back to old, script-based method */}
}
//cribbed this script from VirtualBox's guest additions installer.
//It's designed to launch the system's gui superuser password
//prompt across multiple distributions and desktop environments.

View File

@@ -2,6 +2,7 @@
using LibationFileManager;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace MacOSConfigApp;
@@ -45,12 +46,12 @@ internal class MacOSInterop : IInteropFunctions
public string ReleaseIdString => AppScaffolding.LibationScaffolding.ReleaseIdentifier.ToString();
public void InstallUpgrade(string upgradeBundle)
public Task InstallUpgradeAsync(string upgradeBundle)
{
Serilog.Log.Information($"Extracting upgrade bundle to {AppPath}");
//Upgrade bundle is a DMG
Process.Start("open", upgradeBundle.SurroundWithQuotes())?.WaitForExit();
return Task.Run(() => Process.Start("open", upgradeBundle.SurroundWithQuotes())?.WaitForExit());
}
//Using osascript -e '[script]' works from the terminal, but I haven't figured

View File

@@ -4,6 +4,7 @@ using SixLabors.ImageSharp;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace WindowsConfigApp;
@@ -32,7 +33,7 @@ internal class WinInterop : IInteropFunctions
public string ReleaseIdString => AppScaffolding.LibationScaffolding.ReleaseIdentifier.ToString();
public void InstallUpgrade(string upgradeBundle)
public async Task InstallUpgradeAsync(string upgradeBundle)
{
const string ExtractorExeName = "ZipExtractor.exe";
var thisExe = Environment.ProcessPath;
@@ -44,10 +45,15 @@ internal class WinInterop : IInteropFunctions
File.Copy(Path.Combine(thisDir, ExtractorExeName), zipExtractor, overwrite: true);
RunAsRoot(zipExtractor,
var proc = RunAsRoot(zipExtractor,
$"--input {upgradeBundle.SurroundWithQuotes()} " +
$"--output {thisDir.SurroundWithQuotes()} " +
$"--executable {thisExe.SurroundWithQuotes()}");
if (proc is null)
throw new InvalidOperationException("Could not start the elevated upgrade process.");
await proc.WaitForExitAsync();
if (proc.ExitCode != 0)
throw new InvalidOperationException($"ZipExtractor exited with code {proc.ExitCode}.");
}
public Process? RunAsRoot(string exe, string args)