using AppScaffolding; using LibationFileManager; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; namespace LinuxConfigApp; internal class LinuxInterop : IInteropFunctions { //Different terminal apps possibly installed on a linux system // [0] console executable // [1] argument to set the concole's title // [2] argument to pass a command to be executed to the terminal static readonly string[][] consoleCommands = { new[] {"konsole", "--title", "-e"}, new[] {"gnome-terminal", "--title", "--"}, new[] {"mate-terminal", "--title", "-x"}, new[] {"xterm", "-T", "-e"}, }; public LinuxInterop() { } public LinuxInterop(params object[] values) { } public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException(); public void SetFolderIcon(byte[] imageJpegBytes, string directory) => throw new PlatformNotSupportedException(); public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException(); public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + LinuxPackageFormatSuffix(); //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("/usr/bin/libation") || File.Exists("/bin/libation"); public async Task InstallUpgradeAsync(string upgradeBundle, Version targetVersion) { 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) { var isRpmBundle = string.Equals(Path.GetExtension(upgradeBundle), ".rpm", StringComparison.OrdinalIgnoreCase); var isDebBundle = string.Equals(Path.GetExtension(upgradeBundle), ".deb", StringComparison.OrdinalIgnoreCase); if (isRpmBundle || (!isDebBundle && UsesRpmPackageFormat())) { if (TryFirstExisting(out pkgExe, "/usr/bin/dnf5", "/bin/dnf5") || TryFirstExisting(out pkgExe, "/usr/bin/dnf", "/bin/dnf") || TryFirstExisting(out pkgExe, "/usr/bin/yum", "/bin/yum")) { pkgArgs = new[] { "install", "-y", upgradeBundle }; return true; } } if (isDebBundle || UsesDebPackageFormat()) { 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; } // RHEL/Fedora can have apt installed for .deb files; prefer native RPM managers when present. internal static string LinuxPackageFormatSuffix() => UsesRpmPackageFormat() ? "_RPM" : UsesDebPackageFormat() ? "_DEB" : "_RPM"; private static bool UsesRpmPackageFormat() => TryFirstExisting(out _, "/usr/bin/dnf5", "/bin/dnf5", "/usr/bin/dnf", "/bin/dnf", "/usr/bin/yum", "/bin/yum"); private static bool UsesDebPackageFormat() => TryFirstExisting(out _, "/usr/bin/apt", "/bin/apt"); 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) { if (File.Exists("/usr/bin/pkexec")) { exePath = "/usr/bin/pkexec"; return true; } if (File.Exists("/bin/pkexec")) { exePath = "/bin/pkexec"; return true; } exePath = null; return false; } public Process? RunAsRoot(string exe, string args) { //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. const string runasroot = "runasroot.sh"; string command = $"{exe ?? ""} {args ?? ""}".Trim(); foreach (var console in consoleCommands) { ProcessStartInfo psi = new() { FileName = console[0], UseShellExecute = false, ArgumentList = { console[1], $"Running '{exe}' as root", // console title console[2], "/bin/sh", Path.Combine(Configuration.ProcessDirectory, runasroot), //script file "Installing Libation package", //command title command, // command to execute vis /bin/sh $"Please run '{command}' manually" // error message to display in the terminal } }; try { return Process.Start(psi); } catch { } } throw new PlatformNotSupportedException($"Could not start any of the supported terminals: {string.Join(", ", consoleCommands.Select(c => c[0]))}"); } public void TrySyncInstallMetadata() { } }