diff --git a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs index c15cb3580..041cfcbb7 100644 --- a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs +++ b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs @@ -75,6 +75,9 @@ public class Chocolatey : BaseNuGet Path.Join(CoreData.UniGetUIDataDirectory, "Chocolatey"), ]; + // AttemptFastRepair is a no-op here, so retrying a timed-out choco listing just spawns another (#4974). + protected override bool RetryListingTasksOnTimeout => false; + public Chocolatey() { Capabilities = new ManagerCapabilities @@ -276,6 +279,7 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p); p.Start(); + RegisterListingProcess(p); string? line; List lines = []; @@ -315,6 +319,7 @@ protected override IReadOnlyList _getInstalledPackages_UnSafe() p ); p.Start(); + RegisterListingProcess(p); string? line; List lines = []; diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs index 90bc9c0fe..9070b5692 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text; using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; @@ -263,6 +264,67 @@ public bool IsReady() return _ready && IsEnabled() && Status.Found; } + // Opt out (override => false) when AttemptFastRepair can't recover a timed-out query (issue #4974). + protected virtual bool RetryListingTasksOnTimeout => true; + + // Processes started by the current listing task, so a timeout can kill them instead of orphaning them. + private readonly AsyncLocal?> _listingProcesses = new(); + + // Lets a listing task register a process for kill-on-timeout. No-op when outside a listing task. + protected void RegisterListingProcess(Process process) + { + List? processes = _listingProcesses.Value; + if (processes is null) + return; + lock (processes) + processes.Add(process); + } + + private static void KillListingProcesses(List processes) + { + lock (processes) + foreach (Process p in processes) + { + try + { + if (!p.HasExited) + p.Kill(entireProcessTree: true); + } + catch (Exception ex) + { + Logger.Warn($"Could not kill a timed-out process tree: {ex.Message}"); + } + } + } + + private T RunListingTaskWithTimeout(Func method, string taskName) + { + List processes = []; + var task = Task.Run(() => + { + _listingProcesses.Value = processes; + return method(); + }); + + if (!task.Wait(TimeSpan.FromSeconds(PackageListingTaskTimeout))) + { + if (!Settings.Get(Settings.K.DisableTimeoutOnPackageListingTasks)) + { + KillListingProcesses(processes); + CoreTools.FinalizeDangerousTask(task); + throw new TimeoutException( + $"Task {taskName} for manager {Name} did not finish after " + + $"{PackageListingTaskTimeout} seconds, aborting. You may disable " + + $"timeouts from UniGetUI Advanced Settings" + ); + } + + task.Wait(); + } + + return task.GetAwaiter().GetResult(); + } + /// /// Returns an array of Package objects that the manager lists for the given query. Depending on the manager, the list may /// also include similar results. This method is fail-safe and will return an empty array if an error occurs. @@ -278,23 +340,10 @@ private IReadOnlyList _findPackages(string query, bool SecondAttempt) } try { - var task = Task.Run(() => FindPackages_UnSafe(query)); - if (!task.Wait(TimeSpan.FromSeconds(PackageListingTaskTimeout))) - { - if (!Settings.Get(Settings.K.DisableTimeoutOnPackageListingTasks)) - { - CoreTools.FinalizeDangerousTask(task); - throw new TimeoutException( - $"Task _getInstalledPackages for manager {Name} did not finish after " - + $"{PackageListingTaskTimeout} seconds, aborting. You may disable " - + $"timeouts from UniGetUI Advanced Settings" - ); - } - - task.Wait(); - } - - var packages = task.GetAwaiter().GetResult(); + var packages = RunListingTaskWithTimeout( + () => FindPackages_UnSafe(query), + "_findPackages" + ); Logger.Info( $"Found {packages.Count} available packages from {Name} with the query {query}" ); @@ -302,12 +351,11 @@ private IReadOnlyList _findPackages(string query, bool SecondAttempt) } catch (Exception e) { - if (!SecondAttempt) + while (e is AggregateException) + e = e.InnerException ?? new InvalidOperationException("How did we get here?"); + + if (!SecondAttempt && (RetryListingTasksOnTimeout || e is not TimeoutException)) { - while (e is AggregateException) - e = - e.InnerException - ?? new InvalidOperationException("How did we get here?"); Logger.Warn( $"Manager {DisplayName} failed to find packages with exception {e.GetType().Name}: {e.Message}" ); @@ -342,34 +390,20 @@ private IReadOnlyList _getAvailableUpdates(bool SecondAttempt) Task.Run(RefreshPackageIndexes) .Wait(TimeSpan.FromSeconds(PackageListingTaskTimeout)); - var task = Task.Run(GetAvailableUpdates_UnSafe); - if (!task.Wait(TimeSpan.FromSeconds(PackageListingTaskTimeout))) - { - if (!Settings.Get(Settings.K.DisableTimeoutOnPackageListingTasks)) - { - CoreTools.FinalizeDangerousTask(task); - throw new TimeoutException( - $"Task _getInstalledPackages for manager {Name} did not finish after " - + $"{PackageListingTaskTimeout} seconds, aborting. You may disable " - + $"timeouts from UniGetUI Advanced Settings" - ); - } - - task.Wait(); - } - - var packages = task.GetAwaiter().GetResult(); + var packages = RunListingTaskWithTimeout( + GetAvailableUpdates_UnSafe, + "_getAvailableUpdates" + ); Logger.Info($"Found {packages.Count} available updates from {Name}"); return packages; } catch (Exception e) { - if (!SecondAttempt) + while (e is AggregateException) + e = e.InnerException ?? new InvalidOperationException("How did we get here?"); + + if (!SecondAttempt && (RetryListingTasksOnTimeout || e is not TimeoutException)) { - while (e is AggregateException) - e = - e.InnerException - ?? new InvalidOperationException("How did we get here?"); Logger.Warn( $"Manager {DisplayName} failed to list available updates with exception {e.GetType().Name}: {e.Message}" ); @@ -401,34 +435,20 @@ private IReadOnlyList _getInstalledPackages(bool SecondAttempt) } try { - var task = Task.Run(GetInstalledPackages_UnSafe); - if (!task.Wait(TimeSpan.FromSeconds(PackageListingTaskTimeout))) - { - if (!Settings.Get(Settings.K.DisableTimeoutOnPackageListingTasks)) - { - CoreTools.FinalizeDangerousTask(task); - throw new TimeoutException( - $"Task _getInstalledPackages for manager {Name} did not finish after " - + $"{PackageListingTaskTimeout} seconds, aborting. You may disable " - + $"timeouts from UniGetUI Advanced Settings" - ); - } - - task.Wait(); - } - - var packages = task.GetAwaiter().GetResult(); + var packages = RunListingTaskWithTimeout( + GetInstalledPackages_UnSafe, + "_getInstalledPackages" + ); Logger.Info($"Found {packages.Count} installed packages from {Name}"); return packages; } catch (Exception e) { - if (!SecondAttempt) + while (e is AggregateException) + e = e.InnerException ?? new InvalidOperationException("How did we get here?"); + + if (!SecondAttempt && (RetryListingTasksOnTimeout || e is not TimeoutException)) { - while (e is AggregateException) - e = - e.InnerException - ?? new InvalidOperationException("How did we get here?"); Logger.Warn( $"Manager {DisplayName} failed to list installed packages with exception {e.GetType().Name}: {e.Message}" );