mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-05-08 23:54:10 -04:00
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:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user