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)