Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow installs and removals to be cancelled #4253

Merged
merged 1 commit into from
Nov 7, 2024
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
26 changes: 21 additions & 5 deletions Core/ModuleInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using CKAN.Versioning;
using CKAN.Configuration;
using CKAN.Games;
using System.Threading;

namespace CKAN
{
Expand All @@ -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());
}

Expand Down Expand Up @@ -182,7 +189,7 @@ public void InstallList(ICollection<CkanModule> 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;
Expand Down Expand Up @@ -464,6 +471,10 @@ private List<string> InstallModule(CkanModule module,
var fileProgress = new ProgressImmediate<long>(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);
Expand Down Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions Core/Net/IDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,5 @@ public interface IDownloader

IEnumerable<CkanModule> ModulesAsTheyFinish(ICollection<CkanModule> cached,
ICollection<CkanModule> toDownload);

/// <summary>
/// Cancel any running downloads.
/// </summary>
void CancelDownload();
}
}
26 changes: 12 additions & 14 deletions Core/Net/NetAsyncDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Invoked when a download completes or fails.
Expand All @@ -51,17 +52,19 @@ public partial class NetAsyncDownloader
/// </summary>
public NetAsyncDownloader(IUser user,
Func<HashAlgorithm?> 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<DownloadTarget> downloadTargets,
string? userAgent,
IUser? user = null)
IUser? user = null)
{
var downloader = new NetAsyncDownloader(user ?? new NullUser(), () => null, userAgent);
downloader.onOneCompleted += (target, error, etag, hash) =>
Expand Down Expand Up @@ -123,7 +126,7 @@ public void DownloadAndWait(ICollection<DownloadTarget> 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
Expand Down Expand Up @@ -185,17 +188,6 @@ public void DownloadAndWait(ICollection<DownloadTarget> targets)
log.Debug("Done downloading");
}

/// <summary>
/// <see cref="IDownloader.CancelDownload()"/>
/// This will also call onCompleted with all null arguments.
/// </summary>
public void CancelDownload()
{
log.Info("Cancelling download");
download_canceled = true;
triggerCompleted();
}

/// <summary>
/// Downloads our files.
/// </summary>
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 8 additions & 16 deletions Core/Net/NetAsyncModulesDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ public class NetAsyncModulesDownloader : IDownloader
/// <summary>
/// Returns a perfectly boring NetAsyncModulesDownloader.
/// </summary>
public NetAsyncModulesDownloader(IUser user, NetModuleCache cache, string? userAgent = null)
public NetAsyncModulesDownloader(IUser user,
NetModuleCache cache,
string? userAgent = null,
CancellationToken cancelToken = default)
{
modules = new List<CkanModule>();
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) =>
Expand All @@ -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(
Expand Down Expand Up @@ -94,7 +98,6 @@ public void DownloadModules(IEnumerable<CkanModule> modules)
grp => grp.ToArray());
try
{
cancelTokenSrc = new CancellationTokenSource();
// Start the downloads!
downloader.DownloadAndWait(targetModules.Keys);
this.modules.Clear();
Expand All @@ -117,17 +120,6 @@ public void DownloadModules(IEnumerable<CkanModule> modules)
}
}

/// <summary>
/// <see cref="IDownloader.CancelDownload()"/>
/// </summary>
public void CancelDownload()
{
// Cancel downloads
downloader.CancelDownload();
// Cancel validation/store
cancelTokenSrc?.Cancel();
}

public IEnumerable<CkanModule> ModulesAsTheyFinish(ICollection<CkanModule> cached,
ICollection<CkanModule> toDownload)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
}
}
10 changes: 6 additions & 4 deletions GUI/Main/MainDownload.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<CkanModule> { gm.ToCkanModule() });
e.Result = e.Argument;
}
Expand All @@ -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
Expand All @@ -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();
Expand Down
20 changes: 12 additions & 8 deletions GUI/Main/MainInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,9 +84,17 @@ private void InstallMods(object? sender, DoWorkEventArgs? e)
&& Manager.Cache != null
&& e?.Argument is (List<ModChange> 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;
Expand Down Expand Up @@ -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<string>? possibleConfigOnlyDirs = null;

// Treat whole changeset as atomic
Expand Down
12 changes: 5 additions & 7 deletions GUI/Main/MainRepo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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) =>
{
Expand All @@ -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();
}
Expand Down