diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b6c5565..069822ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - [Multiple] Cache migration and other fixes (#4240 by: HebaruSan) - [Multiple] Start installing mods while downloads are still in progress (#4249 by: HebaruSan) - [Multiple] Sort dependencies first in modpacks (#4252 by: HebaruSan) +- [Multiple] Allow installs and removals to be cancelled (#4253 by: HebaruSan) ### Bugfixes diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 4c6fe27a9..8ab7a4990 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -14,6 +14,7 @@ using CKAN.Versioning; using CKAN.Configuration; using CKAN.Games; +using System.Threading; namespace CKAN { @@ -34,17 +35,23 @@ public class ModuleInstaller private static readonly ILog log = LogManager.GetLogger(typeof(ModuleInstaller)); - private readonly GameInstance instance; - private readonly NetModuleCache Cache; - private readonly string? userAgent; + private readonly GameInstance instance; + private readonly NetModuleCache Cache; + private readonly string? userAgent; + private readonly CancellationToken cancelToken; // Constructor - public ModuleInstaller(GameInstance inst, NetModuleCache cache, IUser user, string? userAgent = null) + public ModuleInstaller(GameInstance inst, + NetModuleCache cache, + IUser user, + string? userAgent = null, + CancellationToken cancelToken = default) { User = user; Cache = cache; instance = inst; this.userAgent = userAgent; + this.cancelToken = cancelToken; log.DebugFormat("Creating ModuleInstaller for {0}", instance.GameDir()); } @@ -182,7 +189,7 @@ public void InstallList(ICollection modules, long installedBytes = 0; if (downloads.Count > 0) { - downloader ??= new NetAsyncModulesDownloader(User, Cache, userAgent); + downloader ??= new NetAsyncModulesDownloader(User, Cache, userAgent, cancelToken); downloader.OverallDownloadProgress += brc => { downloadedBytes = downloadBytes - brc.BytesLeft; @@ -464,6 +471,10 @@ private List InstallModule(CkanModule module, var fileProgress = new ProgressImmediate(bytes => moduleProgress?.Report(installedBytes + bytes)); foreach (InstallableFile file in files) { + if (cancelToken.IsCancellationRequested) + { + throw new CancelledActionKraken(); + } log.DebugFormat("Copying {0}", file.source.Name); var path = CopyZipEntry(zipfile, file.source, file.destination, file.makedir, fileProgress); @@ -947,6 +958,11 @@ private void Uninstall(string identifier, long bytesDeleted = 0; foreach (string relPath in modFiles) { + if (cancelToken.IsCancellationRequested) + { + throw new CancelledActionKraken(); + } + string absPath = instance.ToAbsoluteGameDir(relPath); try diff --git a/Core/Net/IDownloader.cs b/Core/Net/IDownloader.cs index a14ba3ea7..211e3c6ae 100644 --- a/Core/Net/IDownloader.cs +++ b/Core/Net/IDownloader.cs @@ -38,10 +38,5 @@ public interface IDownloader IEnumerable ModulesAsTheyFinish(ICollection cached, ICollection toDownload); - - /// - /// Cancel any running downloads. - /// - void CancelDownload(); } } diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs index ac17c639d..5f5406ab8 100644 --- a/Core/Net/NetAsyncDownloader.cs +++ b/Core/Net/NetAsyncDownloader.cs @@ -37,6 +37,7 @@ public partial class NetAsyncDownloader // For inter-thread communication private volatile bool download_canceled; private readonly ManualResetEvent complete_or_canceled; + private readonly CancellationToken cancelToken; /// /// Invoked when a download completes or fails. @@ -51,17 +52,19 @@ public partial class NetAsyncDownloader /// public NetAsyncDownloader(IUser user, Func getHashAlgo, - string? userAgent = null) + string? userAgent = null, + CancellationToken cancelToken = default) { User = user; this.userAgent = userAgent ?? Net.UserAgentString; this.getHashAlgo = getHashAlgo; + this.cancelToken = cancelToken; complete_or_canceled = new ManualResetEvent(false); } public static void DownloadWithProgress(IList downloadTargets, string? userAgent, - IUser? user = null) + IUser? user = null) { var downloader = new NetAsyncDownloader(user ?? new NullUser(), () => null, userAgent); downloader.onOneCompleted += (target, error, etag, hash) => @@ -123,7 +126,7 @@ public void DownloadAndWait(ICollection targets) log.Debug("Completion signal reset"); // If the user cancelled our progress, then signal that. - if (old_download_canceled) + if (old_download_canceled || cancelToken.IsCancellationRequested) { log.DebugFormat("User clicked cancel, discarding {0} queued downloads: {1}", queuedDownloads.Count, string.Join(", ", queuedDownloads.SelectMany(dl => dl.target.urls))); // Ditch anything we haven't started @@ -185,17 +188,6 @@ public void DownloadAndWait(ICollection targets) log.Debug("Done downloading"); } - /// - /// - /// This will also call onCompleted with all null arguments. - /// - public void CancelDownload() - { - log.Info("Cancelling download"); - download_canceled = true; - triggerCompleted(); - } - /// /// Downloads our files. /// @@ -297,6 +289,12 @@ private void FileProgressReport(DownloadPart download, long bytesDownloaded, lon } OverallProgress?.Invoke(rateCounter); + + if (cancelToken.IsCancellationRequested) + { + download_canceled = true; + triggerCompleted(); + } } private void PopFromQueue(string host) diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index d14f45fbb..10a11fb79 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -29,10 +29,13 @@ public class NetAsyncModulesDownloader : IDownloader /// /// Returns a perfectly boring NetAsyncModulesDownloader. /// - public NetAsyncModulesDownloader(IUser user, NetModuleCache cache, string? userAgent = null) + public NetAsyncModulesDownloader(IUser user, + NetModuleCache cache, + string? userAgent = null, + CancellationToken cancelToken = default) { modules = new List(); - downloader = new NetAsyncDownloader(user, SHA256.Create, userAgent); + downloader = new NetAsyncDownloader(user, SHA256.Create, userAgent, cancelToken); // Schedule us to process each module on completion. downloader.onOneCompleted += ModuleDownloadComplete; downloader.TargetProgress += (target, remaining, total) => @@ -44,6 +47,7 @@ public NetAsyncModulesDownloader(IUser user, NetModuleCache cache, string? userA }; downloader.OverallProgress += brc => OverallDownloadProgress?.Invoke(brc); this.cache = cache; + this.cancelToken = cancelToken; } internal NetAsyncDownloader.DownloadTarget TargetFromModuleGroup( @@ -94,7 +98,6 @@ public void DownloadModules(IEnumerable modules) grp => grp.ToArray()); try { - cancelTokenSrc = new CancellationTokenSource(); // Start the downloads! downloader.DownloadAndWait(targetModules.Keys); this.modules.Clear(); @@ -117,17 +120,6 @@ public void DownloadModules(IEnumerable modules) } } - /// - /// - /// - public void CancelDownload() - { - // Cancel downloads - downloader.CancelDownload(); - // Cancel validation/store - cancelTokenSrc?.Cancel(); - } - public IEnumerable ModulesAsTheyFinish(ICollection cached, ICollection toDownload) { @@ -214,7 +206,7 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, fileSize)), module.StandardName(), false, - cancelTokenSrc?.Token); + cancelToken); File.Delete(filename); foreach (var m in completedMods) { @@ -266,6 +258,6 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, private readonly NetAsyncDownloader downloader; private IUser User => downloader.User; private readonly NetModuleCache cache; - private CancellationTokenSource? cancelTokenSrc; + private CancellationToken cancelToken; } } diff --git a/GUI/Main/MainDownload.cs b/GUI/Main/MainDownload.cs index 4bd7e5c48..f1e2bdc5f 100644 --- a/GUI/Main/MainDownload.cs +++ b/GUI/Main/MainDownload.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Collections.Generic; using System.ComponentModel; +using System.Threading; using System.Threading.Tasks; using CKAN.GUI.Attributes; @@ -46,11 +47,13 @@ private void CacheMod(object? sender, DoWorkEventArgs? e) && e.Argument is GUIMod gm && Manager?.Cache != null) { - downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent); + var cancelTokenSrc = new CancellationTokenSource(); + Wait.OnCancel += cancelTokenSrc.Cancel; + downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent, + cancelTokenSrc.Token); downloader.DownloadProgress += OnModDownloading; downloader.StoreProgress += OnModValidating; downloader.OverallDownloadProgress += currentUser.RaiseProgress; - Wait.OnCancel += downloader.CancelDownload; downloader.DownloadModules(new List { gm.ToCkanModule() }); e.Result = e.Argument; } @@ -60,7 +63,6 @@ public void PostModCaching(object? sender, RunWorkerCompletedEventArgs? e) { if (downloader != null) { - Wait.OnCancel -= downloader.CancelDownload; downloader = null; } // Can't access e.Result if there's an error @@ -69,7 +71,7 @@ public void PostModCaching(object? sender, RunWorkerCompletedEventArgs? e) switch (e.Error) { - case CancelledActionKraken exc: + case CancelledActionKraken: // User already knows they cancelled, get out HideWaitDialog(); EnableMainWindow(); diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 5d5e45c99..9f0d7cc79 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Transactions; +using System.Threading; #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif @@ -83,9 +84,17 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) && Manager.Cache != null && e?.Argument is (List changes, RelationshipResolverOptions options)) { + var cancelTokenSrc = new CancellationTokenSource(); + Wait.OnCancel += () => + { + canceled = true; + cancelTokenSrc.Cancel(); + }; + var registry_manager = RegistryManager.Instance(CurrentInstance, repoData); var registry = registry_manager.registry; - var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser, userAgent); + var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser, userAgent, + cancelTokenSrc.Token); // Avoid accumulating multiple event handlers installer.OneComplete -= OnModInstalled; installer.InstallProgress -= OnModInstalling; @@ -195,16 +204,11 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) }); tabController.SetTabLock(true); - var downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent); + var downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent, + cancelTokenSrc.Token); downloader.DownloadProgress += OnModDownloading; downloader.StoreProgress += OnModValidating; - Wait.OnCancel += () => - { - canceled = true; - downloader.CancelDownload(); - }; - HashSet? possibleConfigOnlyDirs = null; // Treat whole changeset as atomic diff --git a/GUI/Main/MainRepo.cs b/GUI/Main/MainRepo.cs index 3a4254f6c..a9908d827 100644 --- a/GUI/Main/MainRepo.cs +++ b/GUI/Main/MainRepo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Timers; +using System.Threading; using System.Linq; using System.Windows.Forms; using System.Transactions; @@ -66,6 +67,9 @@ private void UpdateRepo(object? sender, DoWorkEventArgs? e) // Note the current mods' compatibility for the NewlyCompatible filter var registry = regMgr.registry; + var cancelTokenSrc = new CancellationTokenSource(); + Wait.OnCancel += cancelTokenSrc.Cancel; + // Load cached data with progress bars instead of without if not already loaded // (which happens if auto-update is enabled, otherwise this is a no-op). // We need the old data to alert the user of newly compatible modules after update. @@ -89,7 +93,6 @@ private void UpdateRepo(object? sender, DoWorkEventArgs? e) var repos = registry.Repositories.Values.ToArray(); try { - bool canceled = false; var downloader = new NetAsyncDownloader(currentUser, () => null, userAgent); downloader.TargetProgress += (target, remaining, total) => { @@ -100,18 +103,13 @@ private void UpdateRepo(object? sender, DoWorkEventArgs? e) Wait.SetProgress(repo.name, remaining, total); } }; - Wait.OnCancel += () => - { - canceled = true; - downloader.CancelDownload(); - }; currentUser.RaiseMessage(Properties.Resources.MainRepoUpdating); var updateResult = repoData.Update(repos, CurrentInstance.game, forceFullRefresh, downloader, currentUser, userAgent); - if (canceled) + if (cancelTokenSrc.Token.IsCancellationRequested) { throw new CancelledActionKraken(); }