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..05f4f8b847 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -603,6 +603,31 @@ 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); + + /// + /// 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 /// @@ -656,6 +681,11 @@ public static async Task CacheUACForCurrentProcess() StandardOutputEncoding = Encoding.UTF8, }, }; + + // 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(); _isCaching = false; 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) {