Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ private static async Task LoadElevatorAsync()
return;
}

if (OperatingSystem.IsLinux())
{
await LoadLinuxElevatorAsync();
return;
}

if (SecureSettings.Get(SecureSettings.K.ForceUserGSudo))
{
var res = await CoreTools.WhichAsync("gsudo.exe");
Expand Down Expand Up @@ -179,6 +185,72 @@ private static async Task LoadElevatorAsync()
}
}

[System.Runtime.Versioning.SupportedOSPlatform("linux")]
private static async Task LoadLinuxElevatorAsync()
{
// Prefer sudo over pkexec: sudo caches credentials on disk (per user, not per
// process), so the user is only prompted once per ~15-minute window regardless
// of how many packages are installed. pkexec prompts on every single invocation
// because polkit ties its authorization cache to the calling process PID.
var results = await Task.WhenAll(
CoreTools.WhichAsync("sudo"),
CoreTools.WhichAsync("pkexec"),
CoreTools.WhichAsync("zenity"));
var (sudoFound, sudoPath) = results[0];
var (pkexecFound, pkexecPath) = results[1];
var (zenityFound, zenityPath) = results[2];

if (sudoFound)
{
// Find a graphical askpass helper so sudo can prompt without a terminal.
// Most DEs (KDE, XFCE, ...) pre-set SSH_ASKPASS to their native tool;
// GNOME doesn't, so we fall back to zenity with a small wrapper script
// (zenity --password ignores positional args, so it needs the wrapper
// to forward the prompt text via --text="$1").
string? askpass = null;
var envAskpass = Environment.GetEnvironmentVariable("SSH_ASKPASS");
if (!string.IsNullOrEmpty(envAskpass) && File.Exists(envAskpass))
askpass = envAskpass;
else if (zenityFound)
{
askpass = Path.Join(CoreData.UniGetUIDataDirectory, "linux-askpass.sh");
await File.WriteAllTextAsync(askpass,
$"#!/bin/sh\n\"{zenityPath}\" --password --title=\"UniGetUI\" --text=\"$1\"\n");
File.SetUnixFileMode(askpass,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}

if (askpass != null)
{
Environment.SetEnvironmentVariable("SUDO_ASKPASS", askpass);
CoreData.ElevatorPath = sudoPath;
CoreData.ElevatorArgs = "-A";
Logger.Debug($"Using sudo -A with askpass '{askpass}'");
return;
}
}

// Fall back to pkexec when no usable sudo+askpass combination is found.
// pkexec handles its own graphical prompt via polkit but prompts every invocation.
if (pkexecFound)
{
CoreData.ElevatorPath = pkexecPath;
Logger.Warn($"Using pkexec at {pkexecPath} (prompts on every operation)");
return;
}

if (sudoFound)
{
CoreData.ElevatorPath = sudoPath;
Logger.Warn($"Falling back to sudo without graphical askpass at {sudoPath}");
return;
}

Logger.Warn("No elevation tool found (pkexec/sudo). Admin operations will fail.");
}

/// <summary>
/// Checks all ready package managers for missing dependencies.
/// Returns the list of dependencies whose installation was not skipped by the user.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ private async Task ResetAll()
await entry.RemoveAsync();
}

private static string ResolveManagerIcon(string managerKey) =>
(managerKey switch
private static string ResolveManagerIcon(string managerKey)
{
string name = managerKey switch
{
"winget" => "winget",
"scoop" => "scoop",
Expand All @@ -123,10 +124,13 @@ private static string ResolveManagerIcon(string managerKey) =>
"steam" => "steam",
"gog" => "gog",
"uplay" => "uplay",
"apt" => "apt",
"dnf" => "dnf",
"pacman" => "pacman",
_ => "ms_store",
}) is var name
? $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg"
: $"avares://UniGetUI.Avalonia/Assets/Symbols/ms_store.svg";
};
return $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg";
}
}

public partial class IgnoredPackageEntryViewModel : ObservableObject
Expand Down
38 changes: 21 additions & 17 deletions src/UniGetUI.Avalonia/Views/Controls/Settings/SettingsPageButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public IconType Icon
{
set => HeaderIcon = new SvgIcon
{
Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{IconTypeToName(value)}.svg",
Path = IconTypeToPath(value),
Width = 24,
Height = 24,
};
Expand All @@ -32,21 +32,25 @@ public SettingsPageButton()
IsClickEnabled = true;
}

private static string IconTypeToName(IconType icon) => icon switch
private static string IconTypeToPath(IconType icon)
{
IconType.Chocolatey => "choco",
IconType.Package => "package",
IconType.UAC => "uac",
IconType.Update => "update",
IconType.Help => "help",
IconType.Console => "console",
IconType.Checksum => "checksum",
IconType.Download => "download",
IconType.Settings => "settings",
IconType.SaveAs => "save_as",
IconType.OpenFolder => "open_folder",
IconType.Experimental => "experimental",
IconType.ClipboardList => "clipboard_list",
_ => icon.ToString().ToLower(),
};
string name = icon switch
{
IconType.Chocolatey => "choco",
IconType.Package => "package",
IconType.UAC => "uac",
IconType.Update => "update",
IconType.Help => "help",
IconType.Console => "console",
IconType.Checksum => "checksum",
IconType.Download => "download",
IconType.Settings => "settings",
IconType.SaveAs => "save_as",
IconType.OpenFolder => "open_folder",
IconType.Experimental => "experimental",
IconType.ClipboardList => "clipboard_list",
_ => icon.ToString().ToLower(),
};
return $"avares://UniGetUI.Avalonia/Assets/Symbols/{name}.svg";
}
}
6 changes: 6 additions & 0 deletions src/UniGetUI.Core.Data/CoreData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ public static string UniGetUIExecutableFile

public static string ElevatorPath = "";

/// <summary>
/// Extra arguments to insert between the elevator binary and the elevated command.
/// For example, "-A" when using sudo with an askpass helper on Linux.
/// </summary>
public static string ElevatorArgs = "";

/// <summary>
/// This method will return the most appropriate data directory.
/// If the new directory exists, it will be used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
"bn": "100%",
"da": "100%",
"el": "100%",
"eo": "100%",
"eo": "99%",
"es": "100%",
"es-MX": "100%",
"et": "100%",
"fa": "100%",
"fil": "100%",
"gl": "100%",
"gl": "99%",
"gu": "100%",
"he": "100%",
"hi": "100%",
Expand All @@ -23,10 +23,10 @@
"ka": "100%",
"kn": "100%",
"ko": "100%",
"ku": "100%",
"ku": "99%",
"lt": "100%",
"mk": "100%",
"mr": "100%",
"mr": "99%",
"nb": "100%",
"nn": "100%",
"pt_PT": "100%",
Expand Down Expand Up @@ -58,4 +58,4 @@
"zh_CN": "100%",
"zh_TW": "100%",
"en": "100%"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,9 @@
"A repository full of tools and executables designed with Microsoft's .NET ecosystem in mind.<br>Contains: <b>.NET related tools and scripts</b>": "A repository full of tools and executables designed with Microsoft's .NET ecosystem in mind.<br>Contains: <b>.NET related tools and scripts</b>",
"NuPkg (zipped manifest)": "NuPkg (zipped manifest)",
"The Missing Package Manager for macOS (or Linux).<br>Contains: <b>Formulae, Casks</b>": "The Missing Package Manager for macOS (or Linux).<br>Contains: <b>Formulae, Casks</b>",
"The default package manager for Debian/Ubuntu-based Linux distributions.<br>Contains: <b>Debian/Ubuntu packages</b>": "The default package manager for Debian/Ubuntu-based Linux distributions.<br>Contains: <b>Debian/Ubuntu packages</b>",
Comment thread
GabrielDuf marked this conversation as resolved.
"The default package manager for RHEL/Fedora-based Linux distributions.<br>Contains: <b>RPM packages</b>": "The default package manager for RHEL/Fedora-based Linux distributions.<br>Contains: <b>RPM packages</b>",
"The default package manager for Arch Linux and its derivatives.<br>Contains: <b>Arch Linux packages</b>": "The default package manager for Arch Linux and its derivatives.<br>Contains: <b>Arch Linux packages</b>",
"Node JS's package manager. Full of libraries and other utilities that orbit the javascript world<br>Contains: <b>Node javascript libraries and other related utilities</b>": "Node JS's package manager. Full of libraries and other utilities that orbit the javascript world<br>Contains: <b>Node javascript libraries and other related utilities</b>",
"Python's library manager. Full of python libraries and other python-related utilities<br>Contains: <b>Python libraries and related utilities</b>": "Python's library manager. Full of python libraries and other python-related utilities<br>Contains: <b>Python libraries and related utilities</b>",
"PowerShell's package manager. Find libraries and scripts to expand PowerShell capabilities<br>Contains: <b>Modules, Scripts, Cmdlets</b>": "PowerShell's package manager. Find libraries and scripts to expand PowerShell capabilities<br>Contains: <b>Modules, Scripts, Cmdlets</b>",
Expand Down
36 changes: 34 additions & 2 deletions src/UniGetUI.Core.Tools/Tools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,29 @@ public static async Task CacheUACForCurrentProcess()
{
_isCaching = true;
Logger.Info("Caching admin rights for process id " + Environment.ProcessId);

var elevatorName = Path.GetFileName(CoreData.ElevatorPath);

// pkexec prompts on every invocation and has no caching protocol.
if (elevatorName == "pkexec")
{
_isCaching = false;
return;
}

// sudo: -v validates/extends the cached timestamp.
// Prepend -A only when the SUDO_ASKPASS helper is configured.
// gsudo / UniGetUI Elevator.exe: use the gsudo cache protocol.
string cacheArgs = elevatorName == "sudo"
? (CoreData.ElevatorArgs.Contains("-A") ? "-Av" : "-v")
: "cache on --pid " + Environment.ProcessId + " -d 1";

using Process p = new()
{
StartInfo = new ProcessStartInfo
{
FileName = CoreData.ElevatorPath,
Arguments = "cache on --pid " + Environment.ProcessId + " -d 1",
Arguments = cacheArgs,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
Expand Down Expand Up @@ -546,12 +563,27 @@ public static async Task ResetUACForCurrentProcess()
Logger.Info(
"Resetting administrator rights cache for process id " + Environment.ProcessId
);

var elevatorName = Path.GetFileName(CoreData.ElevatorPath);

// pkexec prompts on every invocation and has no caching protocol.
if (elevatorName == "pkexec")
{
return;
}

// sudo: -K removes all cached timestamps.
// gsudo / UniGetUI Elevator.exe: use the gsudo cache protocol.
string resetArgs = elevatorName == "sudo"
? "-K"
: "cache off --pid " + Environment.ProcessId;

using Process p = new()
{
StartInfo = new ProcessStartInfo
{
FileName = CoreData.ElevatorPath,
Arguments = "cache off --pid " + Environment.ProcessId,
Arguments = resetArgs,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
Expand Down
3 changes: 3 additions & 0 deletions src/UniGetUI.Interface.Enums/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public enum IconType
Rust = '\uE941',
Vcpkg = '\uE942',
Homebrew = '\uE943',
Apt = '\uE944',
Dnf = '\uE945',
Pacman = '\uE946',
}

public class NotificationArguments
Expand Down
Loading
Loading