From 9504dc1cfb156bf887e813f7bed8c80286288906 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Mon, 20 Jul 2026 10:41:34 -0400 Subject: [PATCH 1/2] surface UAC consent prompt in the foreground --- .../Views/MainWindow.axaml.cs | 7 +++++++ src/UniGetUI.Core.Data/CoreData.cs | 8 +++++++ src/UniGetUI.Core.Tools/Tools.cs | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index cb0387360d..7cef17c3a5 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -14,6 +14,7 @@ using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.Pages; +using UniGetUI.Core.Data; using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; @@ -140,6 +141,12 @@ public MainWindow() _trayService = new TrayService(this); _trayService.UpdateStatus(); + + // Let the elevation code activate us right before a UAC prompt, so we own the foreground + // and can delegate it to the consent UI (#5146). Only activate when we're already on screen; + // don't yank a tray-hidden window forward during silent/background elevation. + CoreData.BringMainWindowToForegroundAsync = () => + Dispatcher.UIThread.InvokeAsync(() => { if (IsVisible) ShowFromTray(); }).GetTask(); } protected override void OnOpened(EventArgs e) diff --git a/src/UniGetUI.Core.Data/CoreData.cs b/src/UniGetUI.Core.Data/CoreData.cs index e164d59354..7409e607c7 100644 --- a/src/UniGetUI.Core.Data/CoreData.cs +++ b/src/UniGetUI.Core.Data/CoreData.cs @@ -427,6 +427,14 @@ public static string UniGetUIExecutableFile /// public static string ElevatorArgs = ""; + /// + /// Set by the UI layer to bring the main window to the foreground and wait until it is. + /// Called right before a UAC prompt is triggered so the app owns the foreground and can + /// delegate it to the consent UI (fixes prompts hiding behind the window with secure desktop off). + /// Null in headless mode, where no window exists to activate. + /// + public static Func? BringMainWindowToForegroundAsync; + /// /// This method will return the most appropriate data directory. /// If the new directory exists, it will be used. diff --git a/src/UniGetUI.Core.Tools/Tools.cs b/src/UniGetUI.Core.Tools/Tools.cs index 05ac720f70..be781cfdbc 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -603,6 +603,14 @@ public static string EnsureSafeQueryString(string query) return new Uri(url); } + // Grants every process the right to set the foreground window, bypassing the + // foreground lock so a UAC consent prompt can surface in front. ASFW_ANY = -1. + private const int ASFW_ANY = -1; + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AllowSetForegroundWindow(int dwProcessId); + /// /// Enables GSudo cache for the current process /// @@ -656,6 +664,19 @@ public static async Task CacheUACForCurrentProcess() StandardOutputEncoding = Encoding.UTF8, }, }; + + // This is the one spot where a UAC consent prompt is raised. With secure desktop + // off, Windows won't let the prompt steal focus from a background console child, + // so it just flashes the taskbar (#5146). Bring our own window to the foreground + // first, then delegate that foreground right so the consent UI can come forward. + if (OperatingSystem.IsWindows()) + { + var bringToFront = CoreData.BringMainWindowToForegroundAsync; + if (bringToFront is not null) + await bringToFront(); + AllowSetForegroundWindow(ASFW_ANY); + } + p.Start(); await p.WaitForExitAsync(); _isCaching = false; From b5130aec47883b3f823eb74d85d5eeea069d6ddb Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Mon, 20 Jul 2026 14:02:50 -0400 Subject: [PATCH 2/2] fix(elevation): bring UAC prompt forward on the direct (uncached) path too --- src/UniGetUI.Core.Tools/Tools.cs | 31 ++++++++++++------- .../AbstractProcessOperation.cs | 8 +++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/UniGetUI.Core.Tools/Tools.cs b/src/UniGetUI.Core.Tools/Tools.cs index be781cfdbc..05f4f8b847 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -611,6 +611,23 @@ public static string EnsureSafeQueryString(string query) [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AllowSetForegroundWindow(int dwProcessId); + /// + /// Windows: bring UniGetUI to the foreground and grant foreground rights so an imminent + /// UAC consent prompt surfaces in front instead of only flashing the taskbar (#5146). + /// No-op elsewhere, and when the app owns no visible foreground window to delegate. + /// Must be called immediately before launching the elevator. + /// + public static async Task PrepareForegroundForElevationAsync() + { + if (!OperatingSystem.IsWindows()) + return; + + var bringToFront = CoreData.BringMainWindowToForegroundAsync; + if (bringToFront is not null) + await bringToFront(); + AllowSetForegroundWindow(ASFW_ANY); + } + /// /// Enables GSudo cache for the current process /// @@ -665,17 +682,9 @@ public static async Task CacheUACForCurrentProcess() }, }; - // This is the one spot where a UAC consent prompt is raised. With secure desktop - // off, Windows won't let the prompt steal focus from a background console child, - // so it just flashes the taskbar (#5146). Bring our own window to the foreground - // first, then delegate that foreground right so the consent UI can come forward. - if (OperatingSystem.IsWindows()) - { - var bringToFront = CoreData.BringMainWindowToForegroundAsync; - if (bringToFront is not null) - await bringToFront(); - AllowSetForegroundWindow(ASFW_ANY); - } + // When admin-rights caching is enabled, the UAC consent prompt is raised here. + // Surface it in front instead of letting it flash unnoticed in the taskbar (#5146). + await PrepareForegroundForElevationAsync(); p.Start(); await p.WaitForExitAsync(); diff --git a/src/UniGetUI.PackageEngine.Operations/AbstractProcessOperation.cs b/src/UniGetUI.PackageEngine.Operations/AbstractProcessOperation.cs index a86613dda5..6b125b6e1e 100644 --- a/src/UniGetUI.PackageEngine.Operations/AbstractProcessOperation.cs +++ b/src/UniGetUI.PackageEngine.Operations/AbstractProcessOperation.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using UniGetUI.Core.Data; using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; @@ -106,6 +107,13 @@ protected override async Task PerformOperation() } CancellationToken.ThrowIfCancellationRequested(); + + // When admin-rights caching is disabled (or a cache miss), the elevator is launched + // directly here and the UAC prompt is raised at this Start() — bring it to the + // foreground too, not just on the cached path (#5146). + if (process.StartInfo.FileName == CoreData.ElevatorPath) + await CoreTools.PrepareForegroundForElevationAsync(); + process.Start(); if (CancellationToken.IsCancellationRequested) {