From ccc74cef567f40d541c9f095c173a95cdeb3d575 Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Mon, 20 Apr 2026 10:20:29 -0400 Subject: [PATCH] #1748: In-app upgrades now wait for the real installer and treat failures as failures. --- .../LibationFileManager/IInteropFunctions.cs | 4 +- .../NullInteropFunctions.cs | 3 +- Source/LibationUiBase/Upgrader.cs | 13 +- .../LoadByOS/LinuxConfigApp/LinuxInterop.cs | 112 +++++++++++++----- .../LoadByOS/MacOSConfigApp/MacOSInterop.cs | 5 +- .../LoadByOS/WindowsConfigApp/WinInterop.cs | 10 +- 6 files changed, 108 insertions(+), 39 deletions(-) diff --git a/Source/LibationFileManager/IInteropFunctions.cs b/Source/LibationFileManager/IInteropFunctions.cs index edbee7c1..45ce975c 100644 --- a/Source/LibationFileManager/IInteropFunctions.cs +++ b/Source/LibationFileManager/IInteropFunctions.cs @@ -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); + /// Waits for the privileged installer where possible and throws if it fails. + Task InstallUpgradeAsync(string upgradeBundle); bool CanUpgrade { get; } string ReleaseIdString { get; } } diff --git a/Source/LibationFileManager/NullInteropFunctions.cs b/Source/LibationFileManager/NullInteropFunctions.cs index 3cd3f078..15e77fe7 100644 --- a/Source/LibationFileManager/NullInteropFunctions.cs +++ b/Source/LibationFileManager/NullInteropFunctions.cs @@ -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(); } diff --git a/Source/LibationUiBase/Upgrader.cs b/Source/LibationUiBase/Upgrader.cs index 8fc780ff..dfd28477 100644 --- a/Source/LibationUiBase/Upgrader.cs +++ b/Source/LibationUiBase/Upgrader.cs @@ -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; diff --git a/Source/LoadByOS/LinuxConfigApp/LinuxInterop.cs b/Source/LoadByOS/LinuxConfigApp/LinuxInterop.cs index 83a1ed56..801d4261 100644 --- a/Source/LoadByOS/LinuxConfigApp/LinuxInterop.cs +++ b/Source/LoadByOS/LinuxConfigApp/LinuxInterop.cs @@ -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(); + 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. diff --git a/Source/LoadByOS/MacOSConfigApp/MacOSInterop.cs b/Source/LoadByOS/MacOSConfigApp/MacOSInterop.cs index 13bd5c00..989521a1 100644 --- a/Source/LoadByOS/MacOSConfigApp/MacOSInterop.cs +++ b/Source/LoadByOS/MacOSConfigApp/MacOSInterop.cs @@ -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 diff --git a/Source/LoadByOS/WindowsConfigApp/WinInterop.cs b/Source/LoadByOS/WindowsConfigApp/WinInterop.cs index cc97654a..7b5900fc 100644 --- a/Source/LoadByOS/WindowsConfigApp/WinInterop.cs +++ b/Source/LoadByOS/WindowsConfigApp/WinInterop.cs @@ -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)