From 987df51ade993af5a189b1ce688491c120dc6c64 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Fri, 25 Oct 2024 16:16:52 -0500 Subject: [PATCH] Start installing mods while downloads are still in progress --- Cmdline/Action/Upgrade.cs | 22 +- Cmdline/ConsoleUser.cs | 22 +- ConsoleUI/InstallScreen.cs | 4 +- ConsoleUI/Toolkit/ConsoleScreen.cs | 11 +- Core/Extensions/DictionaryExtensions.cs | 9 + Core/ModuleInstaller.cs | 408 ++++++++++++------ Core/Net/ByteRateCounter.cs | 73 ++++ Core/Net/IDownloader.cs | 12 +- Core/Net/NetAsyncDownloader.DownloadPart.cs | 25 +- Core/Net/NetAsyncDownloader.cs | 96 ++--- Core/Net/NetAsyncModulesDownloader.cs | 116 +++-- Core/Net/NetModuleCache.cs | 29 +- Core/Properties/Resources.resx | 9 +- Core/Relationships/RelationshipResolver.cs | 9 + .../ProgressScalePercentsByFileSize.cs | 5 + Core/Types/Kraken.cs | 60 +-- Core/User.cs | 4 +- GUI/Controls/EditModSearchDetails.cs | 18 - GUI/Controls/LabeledProgressBar.cs | 63 +++ GUI/Controls/TransparentLabel.cs | 69 +++ GUI/Controls/Wait.Designer.cs | 89 ++-- GUI/Controls/Wait.cs | 251 ++++++----- GUI/Enums/WindowExStyles.cs | 15 + GUI/Enums/WindowStyles.cs | 13 + GUI/GUIUser.cs | 7 +- GUI/Main/MainDownload.cs | 9 +- GUI/Main/MainInstall.cs | 69 ++- GUI/Main/MainRepo.cs | 4 +- GUI/Main/MainWait.cs | 5 +- GUI/Properties/Resources.resx | 3 + Netkan/ConsoleUser.cs | 13 +- Tests/CapturingUser.cs | 6 +- Tests/Core/ModuleInstallerDirTest.cs | 2 +- Tests/Core/ModuleInstallerTests.cs | 22 +- Tests/Core/Net/NetModuleCacheTests.cs | 6 +- Tests/GUI/Model/ModList.cs | 2 +- 36 files changed, 980 insertions(+), 600 deletions(-) create mode 100644 Core/Net/ByteRateCounter.cs create mode 100644 GUI/Controls/LabeledProgressBar.cs create mode 100644 GUI/Controls/TransparentLabel.cs create mode 100644 GUI/Enums/WindowExStyles.cs create mode 100644 GUI/Enums/WindowStyles.cs diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 1bd1462cf6..8ac5be431e 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -184,18 +184,18 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) /// IUser object for output /// Game instance to use /// List of modules to upgrade - private void UpgradeModules(NetModuleCache cache, - string? userAgent, - IUser user, - CKAN.GameInstance instance, - List modules) + private void UpgradeModules(NetModuleCache cache, + string? userAgent, + IUser user, + CKAN.GameInstance instance, + List modules) { UpgradeModules( cache, userAgent, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet? possibleConfigOnlyDirs) => installer.Upgrade(modules, downloader, ref possibleConfigOnlyDirs, - regMgr, true, true, true), + regMgr, true, true), modules.Add); } @@ -206,11 +206,11 @@ private void UpgradeModules(NetModuleCache cache, /// IUser object for output /// Game instance to use /// List of identifier[=version] to upgrade - private void UpgradeModules(NetModuleCache cache, - string? userAgent, - IUser user, - CKAN.GameInstance instance, - List identsAndVersions) + private void UpgradeModules(NetModuleCache cache, + string? userAgent, + IUser user, + CKAN.GameInstance instance, + List identsAndVersions) { UpgradeModules( cache, userAgent, user, instance, repoData, diff --git a/Cmdline/ConsoleUser.cs b/Cmdline/ConsoleUser.cs index 8159699c70..27168c8e3c 100644 --- a/Cmdline/ConsoleUser.cs +++ b/Cmdline/ConsoleUser.cs @@ -258,29 +258,21 @@ public void RaiseProgress(string message, int percent) { if (message != lastProgressMessage) { - // The percent looks weird on non-download messages. - // The leading newline makes sure we don't end up with a mess from previous - // download messages. GoToStartOfLine(); - Console.Write("{0}", message); + // The percent looks weird on non-download messages + Console.WriteLine("{0}", message); lastProgressMessage = message; } - - // This message leaves the cursor at the end of a line of text - atStartOfLine = false; + atStartOfLine = true; } - public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) + public void RaiseProgress(ByteRateCounter rateCounter) { - if (!Headless || percent != previousPercent) + if (!Headless || rateCounter.Percent != previousPercent) { - GoToStartOfLine(); - var fullMsg = string.Format(CKAN.Properties.Resources.NetAsyncDownloaderProgress, - CkanModule.FmtSize(bytesPerSecond), - CkanModule.FmtSize(bytesLeft)); // The \r at the front here causes download messages to *overwrite* each other. - Console.Write("\r{0} - {1}% ", fullMsg, percent); - previousPercent = percent; + Console.Write("\r{0} ", rateCounter.Summary); + previousPercent = rateCounter.Percent; // This message leaves the cursor at the end of a line of text atStartOfLine = false; diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index f879f5ba74..e3595c642f 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -76,7 +76,7 @@ public override void Run(Action? process = null) HashSet? possibleConfigOnlyDirs = null; ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this, userAgent); - inst.onReportModInstalled += OnModInstalled; + inst.OneComplete += OnModInstalled; if (plan.Remove.Count > 0) { inst.UninstallList(plan.Remove, ref possibleConfigOnlyDirs, regMgr, true, new List(plan.Install)); plan.Remove.Clear(); @@ -113,7 +113,7 @@ public override void Run(Action? process = null) } trans.Complete(); - inst.onReportModInstalled -= OnModInstalled; + inst.OneComplete -= OnModInstalled; // Don't let the installer re-use old screen references inst.User = new NullUser(); diff --git a/ConsoleUI/Toolkit/ConsoleScreen.cs b/ConsoleUI/Toolkit/ConsoleScreen.cs index 1066d51813..161a6d7b4d 100644 --- a/ConsoleUI/Toolkit/ConsoleScreen.cs +++ b/ConsoleUI/Toolkit/ConsoleScreen.cs @@ -223,15 +223,10 @@ public void RaiseProgress(string message, int percent) /// /// Update a user visible progress bar /// - /// Value 0-100 representing the progress - /// Current download rate - /// Bytes remaining in the downloads - public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) + /// Object with the progress info + public void RaiseProgress(ByteRateCounter rateCounter) { - var fullMsg = string.Format(CKAN.Properties.Resources.NetAsyncDownloaderProgress, - CkanModule.FmtSize(bytesPerSecond), - CkanModule.FmtSize(bytesLeft)); - Progress(fullMsg, percent); + Progress(rateCounter.Summary, rateCounter.Percent); Draw(); } diff --git a/Core/Extensions/DictionaryExtensions.cs b/Core/Extensions/DictionaryExtensions.cs index a6b86710c9..7f20cdc426 100644 --- a/Core/Extensions/DictionaryExtensions.cs +++ b/Core/Extensions/DictionaryExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Collections.Generic; @@ -23,5 +24,13 @@ public static bool DictionaryEquals(this IDictionary? a, return val; } + public static IEnumerable> KeyZip(this IDictionary source, + IDictionary other) + where K : notnull + => source.Select(kvp => other.TryGetValue(kvp.Key, out V2? val2) + ? Tuple.Create(kvp.Key, kvp.Value, val2) + : null) + .OfType>(); + } } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index acba338784..4c6fe27a99 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -28,7 +28,9 @@ public class ModuleInstaller { public IUser User { get; set; } - public event Action? onReportModInstalled = null; + public event Action? InstallProgress; + public event Action? RemoveProgress; + public event Action? OneComplete; private static readonly ILog log = LogManager.GetLogger(typeof(ModuleInstaller)); @@ -71,7 +73,7 @@ public static string Download(CkanModule module, string filename, string? userAg .First(), userAgent); - return cache.Store(module, tmp_file, new ProgressImmediate(percent => {}), filename, true); + return cache.Store(module, tmp_file, new ProgressImmediate(bytes => {}), filename, true); } /// @@ -105,19 +107,6 @@ public static string CachedOrDownload(CkanModule module, string? userAgent, NetM return full_path; } - /// - /// Makes sure all the specified mods are downloaded. - /// - private void DownloadModules(IEnumerable mods, IDownloader downloader) - { - var downloads = mods.Where(module => !module.IsMetapackage && !Cache.IsCached(module)) - .ToList(); - if (downloads.Count > 0) - { - downloader.DownloadModules(downloads); - } - } - #endregion #region Installation @@ -138,7 +127,6 @@ public void InstallList(ICollection modules, IDownloader? downloader = null, bool ConfirmPrompt = true) { - // TODO: Break this up into smaller pieces! It's huge! if (modules.Count == 0) { User.RaiseProgress(Properties.Resources.ModuleInstallerNothingToInstall, 100); @@ -149,73 +137,85 @@ public void InstallList(ICollection modules, instance.game, instance.VersionCriteria()); var modsToInstall = resolver.ModList().ToList(); - var downloads = new List(); + // Alert about attempts to install DLC before downloading or installing anything + if (modsToInstall.Any(m => m.IsDLC)) + { + throw new BadCommandKraken(Properties.Resources.ModuleInstallerDLC); + } // Make sure we have enough space to install this stuff + var installBytes = modsToInstall.Sum(m => m.install_size); CKANPathUtils.CheckFreeSpace(new DirectoryInfo(instance.GameDir()), - modsToInstall.Select(m => m.install_size) - .OfType() - .Sum(), + installBytes, Properties.Resources.NotEnoughSpaceToInstall); - // TODO: All this user-stuff should be happening in another method! - // We should just be installing mods as a transaction. - + var cached = new List(); + var downloads = new List(); User.RaiseMessage(Properties.Resources.ModuleInstallerAboutToInstall); User.RaiseMessage(""); - foreach (var module in modsToInstall) { User.RaiseMessage(" * {0}", Cache.DescribeAvailability(module)); - // Alert about attempts to install DLC before downloading or installing anything - CheckKindInstallationKraken(module); if (!module.IsMetapackage && !Cache.IsMaybeCachedZip(module)) { downloads.Add(module); } + else + { + cached.Add(module); + } } - if (ConfirmPrompt && !User.RaiseYesNoDialog(Properties.Resources.ModuleInstallerContinuePrompt)) { throw new CancelledActionKraken(Properties.Resources.ModuleInstallerUserDeclined); } + var downloadBytes = CkanModule.GroupByDownloads(downloads) + .Sum(grp => grp.First().download_size); + var rateCounter = new ByteRateCounter() + { + Size = downloadBytes + installBytes, + BytesLeft = downloadBytes + installBytes, + }; + rateCounter.Start(); + long downloadedBytes = 0; + long installedBytes = 0; if (downloads.Count > 0) { downloader ??= new NetAsyncModulesDownloader(User, Cache, userAgent); - downloader.DownloadModules(downloads); + downloader.OverallDownloadProgress += brc => + { + downloadedBytes = downloadBytes - brc.BytesLeft; + rateCounter.BytesLeft = downloadBytes - downloadedBytes + + installBytes - installedBytes; + User.RaiseProgress(rateCounter); + }; } - // Make sure we STILL have enough space to install this stuff - // now that the downloads have been stored to the cache - CKANPathUtils.CheckFreeSpace(new DirectoryInfo(instance.GameDir()), - modsToInstall.Select(m => m.install_size) - .OfType() - .Sum(), - Properties.Resources.NotEnoughSpaceToInstall); - // We're about to install all our mods; so begin our transaction. using (var transaction = CkanTransaction.CreateTransactionScope()) { - CkanModule? installing = null; - var progress = new ProgressScalePercentsByFileSizes( - new ProgressImmediate(p => User.RaiseProgress( - string.Format(Properties.Resources.ModuleInstallerInstallingMod, - installing), - // The post-install steps start at 90%, - // so count up to 85% for installation - p * 85 / 100)), - modsToInstall.Select(m => m.install_size)); - for (int i = 0; i < modsToInstall.Count; i++) + var gameDir = new DirectoryInfo(instance.GameDir()); + long modInstallCompletedBytes = 0; + foreach (var mod in ModsInDependencyOrder(resolver, cached, downloads, downloader)) { - installing = modsToInstall[i]; - Install(installing, - resolver.IsAutoInstalled(installing), + // Re-check that there's enough free space in case game dir and cache are on same drive + CKANPathUtils.CheckFreeSpace(gameDir, mod.install_size, + Properties.Resources.NotEnoughSpaceToInstall); + Install(mod, resolver.IsAutoInstalled(mod), registry_manager.registry, ref possibleConfigOnlyDirs, - progress); - progress?.NextFile(); + new ProgressImmediate(bytes => + { + InstallProgress?.Invoke(mod, mod.install_size - bytes, mod.install_size); + installedBytes = modInstallCompletedBytes + bytes; + rateCounter.BytesLeft = downloadBytes - downloadedBytes + + installBytes - installedBytes; + User.RaiseProgress(rateCounter); + })); + modInstallCompletedBytes += mod.install_size; } + rateCounter.Stop(); User.RaiseProgress(Properties.Resources.ModuleInstallerUpdatingRegistry, 90); registry_manager.Save(!options.without_enforce_consistency); @@ -228,6 +228,66 @@ public void InstallList(ICollection modules, User.RaiseProgress(Properties.Resources.ModuleInstallerDone, 100); } + private static IEnumerable ModsInDependencyOrder(RelationshipResolver resolver, + ICollection cached, + ICollection toDownload, + IDownloader? downloader) + + => ModsInDependencyOrder(resolver, cached, + downloader != null && toDownload.Count > 0 + ? downloader.ModulesAsTheyFinish(cached, toDownload) + : null); + + private static IEnumerable ModsInDependencyOrder(RelationshipResolver resolver, + ICollection cached, + IEnumerable? downloading) + { + var waiting = new HashSet(); + var done = new HashSet(); + if (downloading != null) + { + foreach (var newlyCached in downloading) + { + waiting.Add(newlyCached); + foreach (var m in OnePass(resolver, waiting, done)) + { + yield return m; + } + } + } + else + { + waiting.UnionWith(cached); + foreach (var m in OnePass(resolver, waiting, done)) + { + yield return m; + } + } + } + + private static IEnumerable OnePass(RelationshipResolver resolver, + HashSet waiting, + HashSet done) + { + while (true) + { + var newlyDone = waiting.Where(m => resolver.ReadyToInstall(m, done)) + .OrderBy(m => m.identifier) + .ToArray(); + if (newlyDone.Length == 0) + { + // No mods ready to install + break; + } + foreach (var m in newlyDone) + { + waiting.Remove(m); + done.Add(m); + yield return m; + } + } + } + /// /// Install our mod from the filename supplied. /// If no file is supplied, we will check the cache or throw FileNotFoundKraken. @@ -245,7 +305,7 @@ private void Install(CkanModule module, bool autoInstalled, Registry registry, ref HashSet? possibleConfigOnlyDirs, - IProgress? progress) + IProgress? progress) { CheckKindInstallationKraken(module); var version = registry.InstalledVersion(module.identifier); @@ -254,7 +314,7 @@ private void Install(CkanModule module, if (version is not null and not UnmanagedModuleVersion) { User.RaiseMessage(Properties.Resources.ModuleInstallerAlreadyInstalled, - module.identifier, version); + module.name, version); return; } @@ -273,6 +333,9 @@ private void Install(CkanModule module, } } + User.RaiseMessage(Properties.Resources.ModuleInstallerInstallingMod, + module.name); + using (var transaction = CkanTransaction.CreateTransactionScope()) { // Install all the things! @@ -289,8 +352,11 @@ private void Install(CkanModule module, transaction.Complete(); } + User.RaiseMessage(Properties.Resources.ModuleInstallerInstalledMod, + module.name); + // Fire our callback that we've installed a module, if we have one. - onReportModInstalled?.Invoke(module); + OneComplete?.Invoke(module); } /// @@ -317,7 +383,7 @@ private List InstallModule(CkanModule module, string? zip_filename, Registry registry, ref HashSet? possibleConfigOnlyDirs, - IProgress? moduleProgress) + IProgress? moduleProgress) { var createdPaths = new List(); if (module.IsMetapackage || zip_filename == null) @@ -394,16 +460,14 @@ private List InstallModule(CkanModule module, } } } - var fileProgress = moduleProgress != null - ? new ProgressFilesOffsetsToPercent(moduleProgress, - files.Select(f => f.source.Size)) - : null; + long installedBytes = 0; + var fileProgress = new ProgressImmediate(bytes => moduleProgress?.Report(installedBytes + bytes)); foreach (InstallableFile file in files) { log.DebugFormat("Copying {0}", file.source.Name); var path = CopyZipEntry(zipfile, file.source, file.destination, file.makedir, fileProgress); - fileProgress?.NextFile(); + installedBytes += file.source.Size; if (path != null) { createdPaths.Add(path); @@ -803,18 +867,38 @@ public void UninstallList(IEnumerable mods, using (var transaction = CkanTransaction.CreateTransactionScope()) { var registry = registry_manager.registry; - var progDescr = ""; - var progress = new ProgressScalePercentsByFileSizes( - new ProgressImmediate(percent => User.RaiseProgress(progDescr, percent)), - goners.Select(id => (long?)registry.InstalledModule(id)?.Files.Count() - ?? 0)); + long removeBytes = goners.Select(registry.InstalledModule) + .OfType() + .Sum(m => m.Module.install_size); + var rateCounter = new ByteRateCounter() + { + Size = removeBytes, + BytesLeft = removeBytes, + }; + rateCounter.Start(); + + long modRemoveCompletedBytes = 0; foreach (string ident in goners) { - progDescr = string.Format(Properties.Resources.ModuleInstallerRemovingMod, - registry.InstalledModule(ident)?.Module.ToString() - ?? ident); - Uninstall(ident, ref possibleConfigOnlyDirs, registry, progress); - progress.NextFile(); + if (registry.InstalledModule(ident) is InstalledModule instMod) + { + User.RaiseMessage(Properties.Resources.ModuleInstallerRemovingMod, + registry.InstalledModule(ident)?.Module.name + ?? ident); + Uninstall(ident, ref possibleConfigOnlyDirs, registry, + new ProgressImmediate(bytes => + { + RemoveProgress?.Invoke(instMod, + instMod.Module.install_size - bytes, + instMod.Module.install_size); + rateCounter.BytesLeft = removeBytes - (modRemoveCompletedBytes + bytes); + User.RaiseProgress(rateCounter); + })); + modRemoveCompletedBytes += instMod?.Module.install_size ?? 0; + User.RaiseMessage(Properties.Resources.ModuleInstallerRemovedMod, + registry.InstalledModule(ident)?.Module.name + ?? ident); + } } // Enforce consistency if we're not installing anything, @@ -837,7 +921,7 @@ public void UninstallList(IEnumerable mods, private void Uninstall(string identifier, ref HashSet? possibleConfigOnlyDirs, Registry registry, - IProgress progress) + IProgress progress) { var file_transaction = new TxFileManager(); @@ -860,11 +944,10 @@ private void Uninstall(string identifier, // Files that Windows refused to delete due to locking (probably) var undeletableFiles = new List(); - int i = 0; + long bytesDeleted = 0; foreach (string relPath in modFiles) { string absPath = instance.ToAbsoluteGameDir(relPath); - progress?.Report(100 * i++ / modFiles.Length); try { @@ -884,6 +967,8 @@ private void Uninstall(string identifier, directoriesToDelete.Add(p); } + bytesDeleted += new FileInfo(absPath).Length; + progress.Report(bytesDeleted); log.DebugFormat("Removing {0}", relPath); file_transaction.Delete(absPath); } @@ -1111,50 +1196,92 @@ public HashSet AddParentDirectories(HashSet directories) /// true or false for each item in `add` /// Modules to remove /// true if newly installed modules should be marked auto-installed, false otherwise - private void AddRemove(ref HashSet? possibleConfigOnlyDirs, - RegistryManager registry_manager, - ICollection add, - ICollection autoInstalled, - ICollection remove, - bool enforceConsistency) + private void AddRemove(ref HashSet? possibleConfigOnlyDirs, + RegistryManager registry_manager, + RelationshipResolver resolver, + ICollection add, + IDictionary autoInstalled, + ICollection remove, + IDownloader downloader, + bool enforceConsistency) { - // TODO: We should do a consistency check up-front, rather than relying - // upon our registry catching inconsistencies at the end. - using (var tx = CkanTransaction.CreateTransactionScope()) { - int totSteps = remove.Count + add.Count; - - string progDescr = ""; - var rmProgress = new ProgressScalePercentsByFileSizes( - new ProgressImmediate(percent => - User.RaiseProgress(progDescr, - percent * add.Count / totSteps)), - remove.Select(instMod => (long)instMod.Files.Count())); - foreach (InstalledModule instMod in remove) + var groups = add.GroupBy(m => m.IsMetapackage || Cache.IsCached(m)); + var cached = groups.FirstOrDefault(grp => grp.Key)?.ToArray() + ?? Array.Empty(); + var toDownload = groups.FirstOrDefault(grp => !grp.Key)?.ToArray() + ?? Array.Empty(); + + long removeBytes = remove.Sum(m => m.Module.install_size); + long removedBytes = 0; + long downloadBytes = toDownload.Sum(m => m.download_size); + long downloadedBytes = 0; + long installBytes = add.Sum(m => m.install_size); + long installedBytes = 0; + var rateCounter = new ByteRateCounter() { - progDescr = string.Format(Properties.Resources.ModuleInstallerRemovingMod, instMod.Module); - Uninstall(instMod.Module.identifier, ref possibleConfigOnlyDirs, registry_manager.registry, rmProgress); - rmProgress.NextFile(); + Size = removeBytes + downloadBytes + installBytes, + BytesLeft = removeBytes + downloadBytes + installBytes, + }; + rateCounter.Start(); + + downloader.OverallDownloadProgress += brc => + { + downloadedBytes = downloadBytes - brc.BytesLeft; + rateCounter.BytesLeft = removeBytes - removedBytes + + downloadBytes - downloadedBytes + + installBytes - installedBytes; + User.RaiseProgress(rateCounter); + }; + var toInstall = ModsInDependencyOrder(resolver, cached, toDownload, downloader); + + long modRemoveCompletedBytes = 0; + foreach (var instMod in remove) + { + Uninstall(instMod.Module.identifier, + ref possibleConfigOnlyDirs, + registry_manager.registry, + new ProgressImmediate(bytes => + { + RemoveProgress?.Invoke(instMod, + instMod.Module.install_size - bytes, + instMod.Module.install_size); + removedBytes = modRemoveCompletedBytes + bytes; + rateCounter.BytesLeft = removeBytes - removedBytes + + downloadBytes - downloadedBytes + + installBytes - installedBytes; + User.RaiseProgress(rateCounter); + })); + modRemoveCompletedBytes += instMod.Module.install_size; } - var addProgress = new ProgressScalePercentsByFileSizes( - new ProgressImmediate(percent => - User.RaiseProgress(progDescr, - ((100 * add.Count) + (percent * remove.Count)) / totSteps)), - add.Select(m => m.install_size)); - foreach ((CkanModule module, bool autoInst) in add.Zip(autoInstalled)) + var gameDir = new DirectoryInfo(instance.GameDir()); + long modInstallCompletedBytes = 0; + foreach (var mod in toInstall) { - progDescr = string.Format(Properties.Resources.ModuleInstallerInstallingMod, module); - var previous = remove?.FirstOrDefault(im => im.Module.identifier == module.identifier); - // For upgrading, new modules are dependencies and should be marked auto-installed, - // for replacing, new modules are the replacements and should not be marked auto-installed - Install(module, - previous?.AutoInstalled ?? autoInst, + CKANPathUtils.CheckFreeSpace(gameDir, mod.install_size, + Properties.Resources.NotEnoughSpaceToInstall); + Install(mod, + // For upgrading, new modules are dependencies and should be marked auto-installed, + // for replacing, new modules are the replacements and should not be marked auto-installed + remove?.FirstOrDefault(im => im.Module.identifier == mod.identifier) + ?.AutoInstalled + ?? autoInstalled[mod], registry_manager.registry, ref possibleConfigOnlyDirs, - addProgress); - addProgress.NextFile(); + new ProgressImmediate(bytes => + { + InstallProgress?.Invoke(mod, + mod.install_size - bytes, + mod.install_size); + installedBytes = modInstallCompletedBytes + bytes; + rateCounter.BytesLeft = removeBytes - removedBytes + + downloadBytes - downloadedBytes + + installBytes - installedBytes; + User.RaiseProgress(rateCounter); + })); + modInstallCompletedBytes += mod.install_size; } registry_manager.Save(enforceConsistency); @@ -1169,29 +1296,24 @@ private void AddRemove(ref HashSet? possibleConfigOnlyDirs, /// Throws ModuleNotFoundKraken if a module is not installed. /// public void Upgrade(ICollection modules, - IDownloader netAsyncDownloader, + IDownloader downloader, ref HashSet? possibleConfigOnlyDirs, RegistryManager registry_manager, bool enforceConsistency = true, - bool resolveRelationships = false, bool ConfirmPrompt = true) { var registry = registry_manager.registry; - var autoInstalled = modules.ToDictionary(m => m, m => true); - if (resolveRelationships) - { - var resolver = new RelationshipResolver( - modules, - modules.Select(m => registry.InstalledModule(m.identifier)?.Module) - .OfType(), - RelationshipResolverOptions.DependsOnlyOpts(), - registry, - instance.game, - instance.VersionCriteria()); - modules = resolver.ModList().ToArray(); - autoInstalled = modules.ToDictionary(m => m, resolver.IsAutoInstalled); - } + var resolver = new RelationshipResolver( + modules, + modules.Select(m => registry.InstalledModule(m.identifier)?.Module) + .OfType(), + RelationshipResolverOptions.DependsOnlyOpts(), + registry, + instance.game, + instance.VersionCriteria()); + modules = resolver.ModList().ToArray(); + var autoInstalled = modules.ToDictionary(m => m, resolver.IsAutoInstalled); User.RaiseMessage(Properties.Resources.ModuleInstallerAboutToUpgrade); User.RaiseMessage(""); @@ -1304,14 +1426,13 @@ public void Upgrade(ICollection modules, throw new CancelledActionKraken(Properties.Resources.ModuleInstallerUpgradeUserDeclined); } - // Start by making sure we've downloaded everything. - DownloadModules(modules, netAsyncDownloader); - AddRemove(ref possibleConfigOnlyDirs, registry_manager, + resolver, modules, - modules.Select(m => autoInstalled[m]).ToArray(), + autoInstalled, to_remove, + downloader, enforceConsistency); User.RaiseProgress(Properties.Resources.ModuleInstallerDone, 100); } @@ -1324,7 +1445,7 @@ public void Upgrade(ICollection modules, /// Thrown if a module that should be replaced is not installed. public void Replace(IEnumerable replacements, RelationshipResolverOptions options, - IDownloader netAsyncDownloader, + IDownloader downloader, ref HashSet? possibleConfigOnlyDirs, RegistryManager registry_manager, bool enforceConsistency = true) @@ -1338,14 +1459,11 @@ public void Replace(IEnumerable replacements, modsToInstall.Add(repl.ReplaceWith); log.DebugFormat("We want to install {0} as a replacement for {1}", repl.ReplaceWith.identifier, repl.ToReplace.identifier); } - // Start by making sure we've downloaded everything. - DownloadModules(modsToInstall, netAsyncDownloader); // Our replacement involves removing the currently installed mods, then // adding everything that needs installing (which may involve new mods to // satisfy dependencies). - // Let's discover what we need to do with each module! foreach (ModuleReplacement repl in replacements) { @@ -1406,11 +1524,14 @@ public void Replace(IEnumerable replacements, var resolver = new RelationshipResolver(modsToInstall, null, options, registry_manager.registry, instance.game, instance.VersionCriteria()); var resolvedModsToInstall = resolver.ModList().ToList(); + AddRemove(ref possibleConfigOnlyDirs, registry_manager, + resolver, resolvedModsToInstall, - resolvedModsToInstall.Select(m => false).ToArray(), + resolvedModsToInstall.ToDictionary(m => m, m => false), modsToRemove, + downloader, enforceConsistency); User.RaiseProgress(Properties.Resources.ModuleInstallerDone, 100); } @@ -1659,12 +1780,18 @@ public static bool ImportFiles(HashSet files, // Store any new files user.RaiseMessage(" "); var description = ""; - var progress = new ProgressScalePercentsByFileSizes( - new ProgressImmediate(p => - user.RaiseProgress(string.Format(Properties.Resources.ModuleInstallerImporting, - description), - p)), - toStore.Select(tuple => tuple.File.Length)); + long installedBytes = 0; + var rateCounter = new ByteRateCounter() + { + Size = toStore.Sum(tuple => tuple.File.Length), + BytesLeft = toStore.Sum(tuple => tuple.File.Length), + }; + rateCounter.Start(); + var progress = new ProgressImmediate(bytes => + { + rateCounter.BytesLeft = rateCounter.Size - (installedBytes + bytes); + user.RaiseProgress(rateCounter); + }); foreach ((FileInfo fi, CkanModule module) in toStore) { @@ -1675,8 +1802,9 @@ public static bool ImportFiles(HashSet files, move: delete && toStore.Last(tuple => tuple.File == fi).Module == module, // Skip revalidation because we had to check the hashes to get here! validate: false); - progress.NextFile(); + installedBytes += fi.Length; } + rateCounter.Stop(); } // Here we have installable containing mods that can be installed, and the importable files have been stored in cache. diff --git a/Core/Net/ByteRateCounter.cs b/Core/Net/ByteRateCounter.cs new file mode 100644 index 0000000000..230aeb0eee --- /dev/null +++ b/Core/Net/ByteRateCounter.cs @@ -0,0 +1,73 @@ +using System; +using System.Timers; + +namespace CKAN +{ + public class ByteRateCounter + { + public ByteRateCounter() + { + progressTimer.Elapsed += CalculateByteRate; + } + + public long Size { get; set; } + public long BytesLeft { get; set; } + public long BytesPerSecond { get; private set; } + + public int Percent + => Size > 0 ? (int)(100 * (Size - BytesLeft) / Size) : 0; + + public TimeSpan TimeLeft + => BytesPerSecond > 0 ? TimeSpan.FromSeconds(BytesLeft / BytesPerSecond) + : TimeSpan.MaxValue; + + public string TimeLeftString + => TimeLeft switch + { + {TotalHours: >1 and var hours} tls + => string.Format(Properties.Resources.ByteRateCounterHoursMinutesSeconds, + (int)hours, tls.Minutes, tls.Seconds), + {TotalMinutes: >1 and var mins} tls + => string.Format(Properties.Resources.ByteRateCounterMinutesSeconds, + (int)mins, tls.Seconds), + var tls + => string.Format(Properties.Resources.ByteRateCounterSeconds, + tls.Seconds), + }; + + public string Summary => + BytesPerSecond > 0 + ? string.Format(Properties.Resources.ByteRateCounterRateSummary, + CkanModule.FmtSize(BytesPerSecond), + CkanModule.FmtSize(BytesLeft), + TimeLeftString, + Percent) + : string.Format(Properties.Resources.ByteRateCounterSummary, + CkanModule.FmtSize(BytesLeft), + Percent); + + public void Start() => progressTimer.Start(); + public void Stop() => progressTimer.Stop(); + + private void CalculateByteRate(object? sender, ElapsedEventArgs args) + { + var now = DateTime.Now; + var timerSpan = now - lastProgressUpdateTime; + var startSpan = now - startedAt; + var bytesDownloaded = Size - BytesLeft; + var timerBytesChange = bytesDownloaded - lastProgressUpdateSize; + lastProgressUpdateSize = bytesDownloaded; + lastProgressUpdateTime = now; + + var overallRate = bytesDownloaded / startSpan.TotalSeconds; + var timerRate = timerBytesChange / timerSpan.TotalSeconds; + BytesPerSecond = (long)(0.5 * (overallRate + timerRate)); + } + + private readonly DateTime startedAt = DateTime.Now; + private DateTime lastProgressUpdateTime = DateTime.Now; + private long lastProgressUpdateSize = 0; + private readonly Timer progressTimer = new Timer(intervalMs); + private const int intervalMs = 3000; + } +} diff --git a/Core/Net/IDownloader.cs b/Core/Net/IDownloader.cs index 4d947d8c17..a14ba3ea7f 100644 --- a/Core/Net/IDownloader.cs +++ b/Core/Net/IDownloader.cs @@ -14,21 +14,31 @@ public interface IDownloader /// void DownloadModules(IEnumerable modules); + public event Action OverallDownloadProgress; + /// /// Raised when data arrives for a module /// - event Action Progress; + event Action DownloadProgress; /// /// Raised while we are checking that a ZIP is valid /// event Action StoreProgress; + /// + /// Raised when one module finishes + /// + event Action OneComplete; + /// /// Raised when a batch of downloads is all done /// event Action AllComplete; + IEnumerable ModulesAsTheyFinish(ICollection cached, + ICollection toDownload); + /// /// Cancel any running downloads. /// diff --git a/Core/Net/NetAsyncDownloader.DownloadPart.cs b/Core/Net/NetAsyncDownloader.DownloadPart.cs index 2034a4fb6d..4db56a9059 100644 --- a/Core/Net/NetAsyncDownloader.DownloadPart.cs +++ b/Core/Net/NetAsyncDownloader.DownloadPart.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Security.Cryptography; using Autofac; @@ -15,11 +14,8 @@ private class DownloadPart { public readonly DownloadTarget target; - public DateTime lastProgressUpdateTime; - public long lastProgressUpdateSize; - public long bytesLeft; - public long size; - public long bytesPerSecond; + public long bytesLeft; + public long size; public Exception? error; // Number of target URLs already tried and failed @@ -28,8 +24,8 @@ private class DownloadPart /// /// Percentage, bytes received, total bytes to receive /// - public event Action? Progress; - public event Action? Done; + public event Action? Progress; + public event Action? Done; private string mimeType => target.mimeType; private readonly string userAgent; @@ -44,7 +40,6 @@ public DownloadPart(DownloadTarget target, this.userAgent = userAgent ?? ""; this.hasher = hasher; size = bytesLeft = target.size; - lastProgressUpdateTime = DateTime.Now; triedDownloads = 0; } @@ -103,20 +98,22 @@ private void ResetAgent() // Forward progress and completion events to our listeners agent.DownloadProgressChanged += (sender, args) => { - Progress?.Invoke(args.ProgressPercentage, args.BytesReceived, args.TotalBytesToReceive); + Progress?.Invoke(this, args.BytesReceived, args.TotalBytesToReceive); }; agent.DownloadProgress += (percent, bytesReceived, totalBytesToReceive) => { - Progress?.Invoke(percent, bytesReceived, totalBytesToReceive); + Progress?.Invoke(this, bytesReceived, totalBytesToReceive); }; agent.DownloadFileCompleted += (sender, args) => { - Done?.Invoke(sender, args, + Done?.Invoke(this, args.Error, args.Cancelled, args.Cancelled || args.Error != null ? null : agent.ResponseHeaders?.Get("ETag")?.Replace("\"", ""), - BitConverter.ToString(hasher?.Hash ?? Array.Empty()) - .Replace("-", "")); + args.Cancelled || args.Error != null + ? "" + : BitConverter.ToString(hasher?.Hash ?? Array.Empty()) + .Replace("-", "")); }; } } diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs index a56bc9d094..ac17c639dd 100644 --- a/Core/Net/NetAsyncDownloader.cs +++ b/Core/Net/NetAsyncDownloader.cs @@ -24,15 +24,16 @@ public partial class NetAsyncDownloader /// /// Raised when data arrives for a download /// - public event Action? Progress; + public event Action? TargetProgress; + public event Action? OverallProgress; private readonly object dlMutex = new object(); - // NOTE: Never remove anything from this, because closures have indexes into it! - // (Clearing completely after completion is OK) private readonly List downloads = new List(); private readonly List queuedDownloads = new List(); private int completed_downloads; + private readonly ByteRateCounter rateCounter = new ByteRateCounter(); + // For inter-thread communication private volatile bool download_canceled; private readonly ManualResetEvent complete_or_canceled; @@ -77,7 +78,7 @@ public static void DownloadWithProgress(IList downloadTargets, /// Start a new batch of downloads /// /// The downloads to begin - public void DownloadAndWait(IList targets) + public void DownloadAndWait(ICollection targets) { lock (dlMutex) { @@ -102,9 +103,13 @@ public void DownloadAndWait(IList targets) Download(targets); } + rateCounter.Start(); + log.Debug("Waiting for downloads to finish..."); complete_or_canceled.WaitOne(); + rateCounter.Stop(); + log.Debug("Downloads finished"); var old_download_canceled = download_canceled; @@ -211,38 +216,33 @@ private void DownloadModule(DownloadPart dl) { if (!queuedDownloads.Contains(dl)) { - log.DebugFormat("Enqueuing download of {0}", string.Join(", ", dl.target.urls)); + log.DebugFormat("Enqueuing download of {0}", dl.CurrentUri); // Throttled host already downloading, we will get back to this later queuedDownloads.Add(dl); } } else { - log.DebugFormat("Beginning download of {0}", string.Join(", ", dl.target.urls)); + log.DebugFormat("Beginning download of {0}", dl.CurrentUri); lock (dlMutex) { if (!downloads.Contains(dl)) { - // We need a new variable for our closure/lambda, hence index = 1+prev max - int index = downloads.Count; - downloads.Add(dl); // Schedule for us to get back progress reports. - dl.Progress += (ProgressPercentage, BytesReceived, TotalBytesToReceive) => - FileProgressReport(index, BytesReceived, TotalBytesToReceive); + dl.Progress += FileProgressReport; // And schedule a notification if we're done (or if something goes wrong) - dl.Done += (sender, args, etag, hash) => - FileDownloadComplete(index, args.Error, args.Cancelled, etag, hash); + dl.Done += FileDownloadComplete; } queuedDownloads.Remove(dl); } // Encode spaces to avoid confusing URL parsers User.RaiseMessage(Properties.Resources.NetAsyncDownloaderDownloading, - dl.CurrentUri.ToString().Replace(" ", "%20")); + dl.CurrentUri.ToString().Replace(" ", "%20")); // Start the download! dl.Download(); @@ -279,65 +279,24 @@ private void triggerCompleted() /// /// Generates a download progress report. /// - /// Index of the file being downloaded + /// The download that progressed /// The percent complete /// The bytes downloaded /// The total amount of bytes we expect to download - private void FileProgressReport(int index, long bytesDownloaded, long bytesToDownload) + private void FileProgressReport(DownloadPart download, long bytesDownloaded, long bytesToDownload) { - DownloadPart download = downloads[index]; - - DateTime now = DateTime.Now; - TimeSpan timeSpan = now - download.lastProgressUpdateTime; - if (timeSpan.Seconds >= 3.0) - { - long bytesChange = bytesDownloaded - download.lastProgressUpdateSize; - download.lastProgressUpdateSize = bytesDownloaded; - download.lastProgressUpdateTime = now; - download.bytesPerSecond = bytesChange / timeSpan.Seconds; - } - - download.size = bytesToDownload; + download.size = bytesToDownload; download.bytesLeft = download.size - bytesDownloaded; - - Progress?.Invoke(download.target, download.bytesLeft, download.size); - - long totalBytesPerSecond = 0; - long totalBytesLeft = 0; - long totalSize = 0; + TargetProgress?.Invoke(download.target, download.bytesLeft, download.size); lock (dlMutex) { - foreach (var t in downloads) - { - if (t == null) - { - continue; - } - - if (t.bytesLeft > 0) - { - totalBytesPerSecond += t.bytesPerSecond; - } - - totalBytesLeft += t.bytesLeft; - totalSize += t.size; - } - foreach (var dl in queuedDownloads) - { - // Somehow managed to get a NullRef for dl here - if (dl == null) - { - continue; - } - - totalBytesLeft += dl.target.size; - totalSize += dl.target.size; - } + var queuedSize = queuedDownloads.Sum(dl => dl.target.size); + rateCounter.Size = queuedSize + downloads.Sum(dl => dl.size); + rateCounter.BytesLeft = queuedSize + downloads.Sum(dl => dl.bytesLeft); } - int totalPercentage = (int)(((totalSize - totalBytesLeft) * 100) / (totalSize)); - User.RaiseProgress(totalPercentage, totalBytesPerSecond, totalBytesLeft); + OverallProgress?.Invoke(rateCounter); } private void PopFromQueue(string host) @@ -360,13 +319,12 @@ private void PopFromQueue(string host) /// This method gets called back by `WebClient` when a download is completed. /// It in turncalls the onCompleted hook when *all* downloads are finished. /// - private void FileDownloadComplete(int index, - Exception? error, - bool canceled, - string? etag, - string hash) + private void FileDownloadComplete(DownloadPart dl, + Exception? error, + bool canceled, + string? etag, + string hash) { - var dl = downloads[index]; var doneUri = dl.CurrentUri; if (error != null) { diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index e1fc5b346c..d14f45fbbf 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Threading.Tasks; using System.IO; using System.Linq; using System.Threading; @@ -18,8 +20,10 @@ namespace CKAN /// public class NetAsyncModulesDownloader : IDownloader { - public event Action? Progress; + public event Action? DownloadProgress; + public event Action? OverallDownloadProgress; public event Action? StoreProgress; + public event Action? OneComplete; public event Action? AllComplete; /// @@ -31,19 +35,18 @@ public NetAsyncModulesDownloader(IUser user, NetModuleCache cache, string? userA downloader = new NetAsyncDownloader(user, SHA256.Create, userAgent); // Schedule us to process each module on completion. downloader.onOneCompleted += ModuleDownloadComplete; - downloader.Progress += (target, remaining, total) => + downloader.TargetProgress += (target, remaining, total) => { - var mod = modules.FirstOrDefault(m => m.download?.Any(dlUri => target.urls.Contains(dlUri)) - ?? false); - if (mod != null && Progress != null) + if (targetModules?[target].First() is CkanModule mod) { - Progress(mod, remaining, total); + DownloadProgress?.Invoke(mod, remaining, total); } }; + downloader.OverallProgress += brc => OverallDownloadProgress?.Invoke(brc); this.cache = cache; } - internal NetAsyncDownloader.DownloadTargetFile TargetFromModuleGroup( + internal NetAsyncDownloader.DownloadTarget TargetFromModuleGroup( HashSet group, string?[] preferredHosts) => TargetFromModuleGroup(group, group.OrderBy(m => m.identifier).First(), preferredHosts); @@ -83,32 +86,33 @@ public void DownloadModules(IEnumerable modules) this.modules.AddRange(moduleGroups.SelectMany(grp => grp)); var preferredHosts = ServiceLocator.Container.Resolve().PreferredHosts; - var targets = moduleGroups + targetModules = moduleGroups // Skip any group that already has a URL in progress .Where(grp => grp.All(mod => mod.download?.All(dlUri => !activeURLs.Contains(dlUri)) ?? false)) // Each group gets one target containing all the URLs - .Select(grp => TargetFromModuleGroup(grp, preferredHosts)) - .ToArray(); + .ToDictionary(grp => TargetFromModuleGroup(grp, preferredHosts), + grp => grp.ToArray()); try { cancelTokenSrc = new CancellationTokenSource(); // Start the downloads! - downloader.DownloadAndWait(targets); + downloader.DownloadAndWait(targetModules.Keys); this.modules.Clear(); + targetModules.Clear(); AllComplete?.Invoke(); } catch (DownloadErrorsKraken kraken) { // Associate the errors with the affected modules - // Find a module for each target - var targetModules = targets.Select(t => this.modules - .First(m => m.download?.Intersect(t.urls) - .Any() - ?? false)) - .ToList(); - var exc = new ModuleDownloadErrorsKraken(targetModules, kraken); + var exc = new ModuleDownloadErrorsKraken( + kraken.Exceptions + .SelectMany(kvp => targetModules[kvp.Key] + .Select(m => new KeyValuePair( + m, kvp.Value.GetBaseException() ?? kvp.Value))) + .ToList()); // Clear this.modules because we're done with these this.modules.Clear(); + targetModules.Clear(); throw exc; } } @@ -124,22 +128,52 @@ public void CancelDownload() cancelTokenSrc?.Cancel(); } - private static readonly ILog log = LogManager.GetLogger(typeof(NetAsyncModulesDownloader)); + public IEnumerable ModulesAsTheyFinish(ICollection cached, + ICollection toDownload) + { + var (dlTask, blockingQueue) = DownloadsCollection(toDownload); + return ModulesAsTheyFinish(cached, dlTask, blockingQueue); + } - private const string defaultMimeType = "application/octet-stream"; + private static IEnumerable ModulesAsTheyFinish(ICollection cached, + Task dlTask, + BlockingCollection blockingQueue) + { + foreach (var m in cached) + { + yield return m; + } + foreach (var m in blockingQueue.GetConsumingEnumerable()) + { + yield return m; + } + blockingQueue.Dispose(); + if (dlTask.Exception is AggregateException { InnerException: Exception exc }) + { + throw exc; + } + } - private readonly List modules; - private readonly NetAsyncDownloader downloader; - private IUser User => downloader.User; - private readonly NetModuleCache cache; - private CancellationTokenSource? cancelTokenSrc; + private (Task dlTask, BlockingCollection blockingQueue) DownloadsCollection(ICollection toDownload) + { + var blockingQueue = new BlockingCollection(new ConcurrentQueue()); + Action oneComplete = m => blockingQueue.Add(m); + OneComplete += oneComplete; + return (Task.Factory.StartNew(() => DownloadModules(toDownload)) + .ContinueWith(t => + { + blockingQueue.CompleteAdding(); + OneComplete -= oneComplete; + }), + blockingQueue); + } private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, Exception? error, string? etag, string? sha256) { - if (target is NetAsyncDownloader.DownloadTargetFile fileTarget) + if (target is NetAsyncDownloader.DownloadTargetFile fileTarget && targetModules != null) { var url = fileTarget.urls.First(); var filename = fileTarget.filename; @@ -157,9 +191,8 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, CkanModule? module = null; try { - module = modules.First(m => (m.download?.Any(dlUri => dlUri == url) - ?? false) - || m.InternetArchiveDownload == url); + var completedMods = targetModules[fileTarget]; + module = completedMods.First(); // Check hash if defined in module if (module.download_hash?.sha256 != null @@ -172,17 +205,24 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, sha256, module.download_hash.sha256)); } - User.RaiseMessage(Properties.Resources.NetAsyncDownloaderValidating, module); + User.RaiseMessage(Properties.Resources.NetAsyncDownloaderValidating, + module.name); + var fileSize = new FileInfo(filename).Length; cache.Store(module, filename, - new ProgressImmediate(percent => StoreProgress?.Invoke(module, 100 - percent, 100)), + new ProgressImmediate(bytes => StoreProgress?.Invoke(module, + fileSize - bytes, + fileSize)), module.StandardName(), false, cancelTokenSrc?.Token); File.Delete(filename); + foreach (var m in completedMods) + { + OneComplete?.Invoke(m); + } } catch (InvalidModuleFileKraken kraken) { - User.RaiseError("{0}", kraken.ToString()); if (module != null) { // Finish out the progress bar @@ -192,7 +232,7 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, File.Delete(filename); // Tell downloader there is a problem with this file - throw; + throw new DownloadErrorsKraken(target, kraken); } catch (OperationCanceledException exc) { @@ -217,5 +257,15 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, } } + private static readonly ILog log = LogManager.GetLogger(typeof(NetAsyncModulesDownloader)); + + private const string defaultMimeType = "application/octet-stream"; + + private readonly List modules; + private IDictionary? targetModules; + private readonly NetAsyncDownloader downloader; + private IUser User => downloader.User; + private readonly NetModuleCache cache; + private CancellationTokenSource? cancelTokenSrc; } } diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs index 91c46d6687..cab42cbb5f 100644 --- a/Core/Net/NetModuleCache.cs +++ b/Core/Net/NetModuleCache.cs @@ -157,7 +157,7 @@ public string GetFileHashSha256(string filePath, IProgress progress, Cancel /// public string Store(CkanModule module, string path, - IProgress? progress, + IProgress? progress, string? description = null, bool move = false, CancellationToken? cancelToken = default, @@ -184,8 +184,7 @@ public string Store(CkanModule module, cancelToken?.ThrowIfCancellationRequested(); // Check valid CRC - if (!ZipValid(path, out string invalidReason, - new ProgressImmediate(percent => progress?.Report(percent)))) + if (!ZipValid(path, out string invalidReason, progress)) { throw new InvalidModuleFileKraken( module, path, @@ -205,7 +204,7 @@ public string Store(CkanModule module, move) : ""; // Make sure completion is signalled so progress bars go away - progress?.Report(100); + progress?.Report(new FileInfo(path).Length); ModStored?.Invoke(module); return success; } @@ -219,9 +218,9 @@ public string Store(CkanModule module, /// /// True if valid, false otherwise. See invalidReason param for explanation. /// - public static bool ZipValid(string filename, - out string invalidReason, - IProgress? progress) + public static bool ZipValid(string filename, + out string invalidReason, + IProgress? progress) { try { @@ -231,7 +230,9 @@ public static bool ZipValid(string filename, { string? zipErr = null; // Limit progress updates to 100 per ZIP file - long highestPercent = -1; + long totalBytesValidated = 0; + long previousBytesValidated = 0; + long onePercent = new FileInfo(filename).Length / 100; // Perform CRC and other checks if (zip.TestArchive(true, TestStrategy.FindFirstError, (TestStatus st, string msg) => @@ -248,14 +249,16 @@ public static bool ZipValid(string filename, Properties.Resources.NetFileCacheZipError, st.Operation, st.Entry?.Name, msg); } - else if (st.Entry != null && progress != null) + else if (st is { Operation: TestOperation.EntryComplete, + Entry: ZipEntry entry } + && progress != null) { // Report progress - var percent = (int)(100 * st.Entry.ZipFileIndex / zip.Count); - if (percent > highestPercent) + totalBytesValidated += entry.CompressedSize; + if (totalBytesValidated - previousBytesValidated > onePercent) { - progress.Report(percent); - highestPercent = percent; + progress.Report(totalBytesValidated); + previousBytesValidated = totalBytesValidated; } } } diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index 2f574c3c60..297a0a07e3 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -121,9 +121,14 @@ Downloading {0} Failed downloading {0} Invalid URL in Location header: {0} + {0}/sec - {1} ({2}) left - {3}% + {0} left - {1}% + {0} hrs, {1} min, {2} sec + {0} min, {1} sec + {0} sec Downloading {0} ... Download cancelled by user - {0}/sec - downloading - {1} left + {0}/sec - {1} left Failed to download "{0}", trying fallback "{1}" Finished downloading {0}, validating and storing to cache... Cannot find cache directory: {0} @@ -228,6 +233,7 @@ What would you like to do? About to install: User declined install list Installing {0}... + Finished installing {0} Updating registry Committing filesystem changes Rescanning {0} @@ -250,6 +256,7 @@ Overwrite? Continue? Mod removal aborted at user request Removing {0}... + Finished removing {0} About to upgrade: * Install: {0} {1} ({2}, {3} remaining) * Install: {0} {1} ({2}, {3}) diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index 8bd2a0ccb5..019ce5ef23 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -481,6 +481,15 @@ public IEnumerable ModList(bool parallel = true) // Resolve ties in name order .ThenBy(m => m.name); + public bool ReadyToInstall(CkanModule mod, ICollection installed) + => !modlist.Values.Distinct() + .Where(m => m != mod) + .Except(installed) + // Ignore circular dependencies + .Except(allDependers(mod)) + .SelectMany(allDependers) + .Contains(mod); + // The more nodes of the reverse-dependency graph we can paint, the higher up in the list it goes private int totalDependers(CkanModule module) => allDependers(module).Count(); diff --git a/Core/Repositories/ProgressScalePercentsByFileSize.cs b/Core/Repositories/ProgressScalePercentsByFileSize.cs index a6302b67d0..20eee3fb86 100644 --- a/Core/Repositories/ProgressScalePercentsByFileSize.cs +++ b/Core/Repositories/ProgressScalePercentsByFileSize.cs @@ -42,6 +42,11 @@ public void Report(int currentFilePercent) } } + public void StartFile(long size) + { + sizes[currentIndex] = size; + } + /// /// Call this when you move on from one file to the next /// diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index ac0cfa455f..f9676f00ec 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using System.Text; using System.Collections.Generic; using log4net; @@ -333,6 +332,10 @@ public DownloadErrorsKraken(List> { @@ -349,56 +352,17 @@ public DownloadErrorsKraken(NetAsyncDownloader.DownloadTarget target, Exception /// public class ModuleDownloadErrorsKraken : Kraken { - /// - /// Initialize the exception. - /// - /// List of modules that we tried to download - /// Download errors from URL-level downloader - public ModuleDownloadErrorsKraken(IList modules, DownloadErrorsKraken kraken) - : base() - { - foreach ((NetAsyncDownloader.DownloadTarget target, Exception exc) in kraken.Exceptions) - { - foreach (var module in modules.Where(m => m.download?.Intersect(target.urls) - .Any() - ?? false)) - { - Exceptions.Add(new KeyValuePair( - module, - exc.GetBaseException() ?? exc)); - } - } - } - - /// - /// Generate a user friendly description of this error. - /// - /// - /// One or more downloads were unsuccessful: - /// - /// Error downloading Astrogator v0.7.8: The remote server returned an error: (404) Not Found. - /// Etc. - /// - public override string ToString() + public ModuleDownloadErrorsKraken(List> errors) + : base(string.Join(Environment.NewLine, + new string[] { Properties.Resources.KrakenModuleDownloadErrorsHeader, "" } + .Concat(errors.Select(kvp => string.Format(Properties.Resources.KrakenModuleDownloadError, + kvp.Key.ToString(), + kvp.Value.Message))))) { - if (builder == null) - { - builder = new StringBuilder(); - builder.AppendLine(Properties.Resources.KrakenModuleDownloadErrorsHeader); - builder.AppendLine(""); - foreach (KeyValuePair kvp in Exceptions) - { - builder.AppendLine(string.Format( - Properties.Resources.KrakenModuleDownloadError, kvp.Key.ToString(), kvp.Value.Message)); - } - } - return builder.ToString(); + Exceptions = new List>(errors); } - public readonly List> Exceptions - = new List>(); - - private StringBuilder? builder = null; + public readonly List> Exceptions; } /// diff --git a/Core/User.cs b/Core/User.cs index 27130c38f9..f403d671d3 100644 --- a/Core/User.cs +++ b/Core/User.cs @@ -24,7 +24,7 @@ void RaiseError([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string message, params object[] args); void RaiseProgress(string message, int percent); - void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft); + void RaiseProgress(ByteRateCounter rateCounter); void RaiseMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string message, params object[] args); } @@ -64,7 +64,7 @@ public void RaiseProgress(string message, int percent) { } - public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) + public void RaiseProgress(ByteRateCounter rateCounter) { } diff --git a/GUI/Controls/EditModSearchDetails.cs b/GUI/Controls/EditModSearchDetails.cs index 0d6de0197b..c6a3737cf7 100644 --- a/GUI/Controls/EditModSearchDetails.cs +++ b/GUI/Controls/EditModSearchDetails.cs @@ -179,23 +179,5 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) } return base.ProcessCmdKey(ref msg, keyData); } - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles - private enum WindowStyles : uint - { - WS_VISIBLE = 0x10000000, - WS_CHILD = 0x40000000, - WS_POPUP = 0x80000000, - } - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles - private enum WindowExStyles : uint - { - WS_EX_TOPMOST = 0x8, - WS_EX_TOOLWINDOW = 0x80, - WS_EX_CONTROLPARENT = 0x10000, - WS_EX_NOACTIVATE = 0x08000000, - } - } } diff --git a/GUI/Controls/LabeledProgressBar.cs b/GUI/Controls/LabeledProgressBar.cs new file mode 100644 index 0000000000..48fcae5fbe --- /dev/null +++ b/GUI/Controls/LabeledProgressBar.cs @@ -0,0 +1,63 @@ +using System.Drawing; +using System.Windows.Forms; +using System.ComponentModel; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif + +namespace CKAN.GUI +{ + /// + /// https://stackoverflow.com/a/40824778 + /// + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif + public class LabeledProgressBar : ProgressBar + { + public LabeledProgressBar() + : base() + { + SuspendLayout(); + + label = new TransparentLabel() + { + ForeColor = SystemColors.ControlText, + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleCenter, + Text = "", + }; + Controls.Add(label); + + ResumeLayout(false); + PerformLayout(); + } + + [Bindable(false)] + [Browsable(true)] + [DefaultValue("")] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + [EditorBrowsable(EditorBrowsableState.Always)] + // If we use override instead of new, the nullability never matches (!) + public new string? Text + { + get => label.Text; + set => label.Text = value; + } + + // If we use override instead of new, the nullability never matches (!) + public new Font Font + { + get => label.Font; + set => label.Font = value; + } + + public ContentAlignment TextAlign + { + get => label.TextAlign; + set => label.TextAlign = value; + } + + private readonly TransparentLabel label; + } +} diff --git a/GUI/Controls/TransparentLabel.cs b/GUI/Controls/TransparentLabel.cs new file mode 100644 index 0000000000..5bbc63dc8f --- /dev/null +++ b/GUI/Controls/TransparentLabel.cs @@ -0,0 +1,69 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif + +namespace CKAN.GUI +{ + #if NET5_0_OR_GREATER + [SupportedOSPlatform("windows")] + #endif + public class TransparentLabel : Label + { + public TransparentLabel() + { + SetStyle(ControlStyles.SupportsTransparentBackColor + | ControlStyles.ResizeRedraw + | ControlStyles.Opaque + | ControlStyles.AllPaintingInWmPaint, + true); + SetStyle(ControlStyles.OptimizedDoubleBuffer, false); + BackColor = Color.Transparent; + } + + // If we use override instead of new, the nullability never matches (!) + public new string? Text + { + get => base.Text; + set + { + base.Text = value; + Parent?.Invalidate(Bounds, false); + } + } + + // If we use override instead of new, the nullability never matches (!) + public new ContentAlignment TextAlign + { + get => base.TextAlign; + set + { + base.TextAlign = value; + Parent?.Invalidate(Bounds, false); + } + } + + protected override CreateParams CreateParams + { + get + { + var cp = base.CreateParams; + cp.ExStyle |= (int)WindowExStyles.WS_EX_TRANSPARENT; + return cp; + } + } + + protected override void OnMove(EventArgs e) + { + base.OnMove(e); + RecreateHandle(); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + // Do nothing + } + } +} diff --git a/GUI/Controls/Wait.Designer.cs b/GUI/Controls/Wait.Designer.cs index f1dc3a608d..77e9b85aa3 100644 --- a/GUI/Controls/Wait.Designer.cs +++ b/GUI/Controls/Wait.Designer.cs @@ -30,76 +30,76 @@ private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(Wait)); - this.TopPanel = new System.Windows.Forms.Panel(); - this.MessageTextBox = new System.Windows.Forms.TextBox(); - this.DialogProgressBar = new System.Windows.Forms.ProgressBar(); + this.VerticalSplitter = new System.Windows.Forms.SplitContainer(); + this.DialogProgressBar = new CKAN.GUI.LabeledProgressBar(); this.ProgressBarTable = new System.Windows.Forms.TableLayoutPanel(); this.LogTextBox = new System.Windows.Forms.TextBox(); this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.CancelCurrentActionButton = new System.Windows.Forms.Button(); this.RetryCurrentActionButton = new System.Windows.Forms.Button(); this.OkButton = new System.Windows.Forms.Button(); + this.VerticalSplitter.Panel1.SuspendLayout(); + this.VerticalSplitter.Panel2.SuspendLayout(); + this.VerticalSplitter.SuspendLayout(); this.ProgressBarTable.SuspendLayout(); this.BottomButtonPanel.SuspendLayout(); this.SuspendLayout(); // - // TopPanel + // VerticalSplitter // - this.TopPanel.Controls.Add(this.MessageTextBox); - this.TopPanel.Controls.Add(this.DialogProgressBar); - this.TopPanel.Controls.Add(this.ProgressBarTable); - this.TopPanel.Dock = System.Windows.Forms.DockStyle.Top; - this.TopPanel.Name = "TopPanel"; - this.TopPanel.Size = new System.Drawing.Size(500, 85); + this.VerticalSplitter.Dock = System.Windows.Forms.DockStyle.Fill; + this.VerticalSplitter.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; + this.VerticalSplitter.Location = new System.Drawing.Point(0, 35); + this.VerticalSplitter.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.VerticalSplitter.Name = "VerticalSplitter"; + this.VerticalSplitter.Orientation = System.Windows.Forms.Orientation.Horizontal; + this.VerticalSplitter.Size = new System.Drawing.Size(500, 500); + this.VerticalSplitter.SplitterDistance = 50; + this.VerticalSplitter.SplitterWidth = 10; + this.VerticalSplitter.TabStop = false; // - // MessageTextBox + // VerticalSplitter.Panel1 // - this.MessageTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.MessageTextBox.BackColor = System.Drawing.SystemColors.Control; - this.MessageTextBox.ForeColor = System.Drawing.SystemColors.ControlText; - this.MessageTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; - this.MessageTextBox.Enabled = false; - this.MessageTextBox.Location = new System.Drawing.Point(5, 5); - this.MessageTextBox.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.MessageTextBox.Multiline = true; - this.MessageTextBox.Name = "MessageTextBox"; - this.MessageTextBox.ReadOnly = true; - this.MessageTextBox.Size = new System.Drawing.Size(490, 30); - this.MessageTextBox.TabIndex = 0; - this.MessageTextBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Center; - resources.ApplyResources(this.MessageTextBox, "MessageTextBox"); + this.VerticalSplitter.Panel1.Controls.Add(this.DialogProgressBar); + this.VerticalSplitter.Panel1.Controls.Add(this.ProgressBarTable); + this.VerticalSplitter.Panel1MinSize = 50; + // + // VerticalSplitter.Panel2 + // + this.VerticalSplitter.Panel2.Controls.Add(this.LogTextBox); + this.VerticalSplitter.Panel2MinSize = 100; // // DialogProgressBar // this.DialogProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.DialogProgressBar.Location = new System.Drawing.Point(5, 45); + this.DialogProgressBar.Location = new System.Drawing.Point(5, 7); this.DialogProgressBar.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.DialogProgressBar.Font = new System.Drawing.Font(System.Drawing.SystemFonts.MessageBoxFont.Name, 12, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel); this.DialogProgressBar.Minimum = 0; this.DialogProgressBar.Maximum = 100; this.DialogProgressBar.Name = "DialogProgressBar"; - this.DialogProgressBar.Size = new System.Drawing.Size(490, 25); + this.DialogProgressBar.Size = new System.Drawing.Size(490, 28); this.DialogProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; - this.DialogProgressBar.TabIndex = 1; + this.DialogProgressBar.TabIndex = 0; // // ProgressBarTable // - this.ProgressBarTable.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + this.ProgressBarTable.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right | System.Windows.Forms.AnchorStyles.Bottom; this.ProgressBarTable.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.ProgressBarTable.AutoScroll = true; + this.ProgressBarTable.AutoSize = false; this.ProgressBarTable.ColumnCount = 2; this.ProgressBarTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.AutoSize)); this.ProgressBarTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.AutoSize)); - this.ProgressBarTable.Location = new System.Drawing.Point(0, 70); + this.ProgressBarTable.Location = new System.Drawing.Point(0, 40); this.ProgressBarTable.Name = "ProgressBarTable"; - this.ProgressBarTable.Padding = new System.Windows.Forms.Padding(0, 6, 0, 6); - this.ProgressBarTable.Size = new System.Drawing.Size(500, 0); - this.ProgressBarTable.AutoSize = false; - this.ProgressBarTable.AutoScroll = false; - this.ProgressBarTable.VerticalScroll.Visible = false; + this.ProgressBarTable.Padding = new System.Windows.Forms.Padding(1); + this.ProgressBarTable.Size = new System.Drawing.Size(490, 10); + this.ProgressBarTable.VerticalScroll.Visible = true; + this.ProgressBarTable.VerticalScroll.SmallChange = 22; this.ProgressBarTable.HorizontalScroll.Visible = false; - this.ProgressBarTable.TabIndex = 0; + this.ProgressBarTable.TabIndex = 1; // // LogTextBox // @@ -107,6 +107,7 @@ private void InitializeComponent() this.LogTextBox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.LogTextBox.Location = new System.Drawing.Point(14, 89); this.LogTextBox.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.LogTextBox.Padding = new System.Windows.Forms.Padding(4); this.LogTextBox.Multiline = true; this.LogTextBox.Name = "LogTextBox"; this.LogTextBox.ReadOnly = true; @@ -160,8 +161,7 @@ private void InitializeComponent() // Wait // this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; - this.Controls.Add(this.LogTextBox); - this.Controls.Add(this.TopPanel); + this.Controls.Add(this.VerticalSplitter); this.Controls.Add(this.BottomButtonPanel); this.Margin = new System.Windows.Forms.Padding(0, 0, 0, 0); this.Padding = new System.Windows.Forms.Padding(0, 0, 0, 0); @@ -172,15 +172,18 @@ private void InitializeComponent() this.ProgressBarTable.PerformLayout(); this.BottomButtonPanel.ResumeLayout(false); this.BottomButtonPanel.PerformLayout(); + this.VerticalSplitter.Panel1.ResumeLayout(false); + this.VerticalSplitter.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.VerticalSplitter)).EndInit(); + this.VerticalSplitter.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); } #endregion - private System.Windows.Forms.Panel TopPanel; - private System.Windows.Forms.TextBox MessageTextBox; - private System.Windows.Forms.ProgressBar DialogProgressBar; + private System.Windows.Forms.SplitContainer VerticalSplitter; + private CKAN.GUI.LabeledProgressBar DialogProgressBar; private System.Windows.Forms.TableLayoutPanel ProgressBarTable; private System.Windows.Forms.TextBox LogTextBox; private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; diff --git a/GUI/Controls/Wait.cs b/GUI/Controls/Wait.cs index 39db5c4b10..b53b508c5a 100644 --- a/GUI/Controls/Wait.cs +++ b/GUI/Controls/Wait.cs @@ -1,10 +1,9 @@ using System; -using System.ComponentModel; using System.Linq; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing; using System.Windows.Forms; -using Timer = System.Windows.Forms.Timer; #if NET5_0_OR_GREATER using System.Runtime.Versioning; #endif @@ -21,18 +20,16 @@ public partial class Wait : UserControl public Wait() { InitializeComponent(); - progressTimer.Tick += (sender, evt) => ReflowProgressBars(); - + emptyHeight = VerticalSplitter.SplitterDistance; bgWorker.DoWork += DoWork; bgWorker.RunWorkerCompleted += RunWorkerCompleted; } - [ForbidGUICalls] public void StartWaiting(Action mainWork, Action postWork, - bool cancelable, - object? param) + bool cancelable, + object? param) { bgLogic = mainWork; postLogic = postWork; @@ -53,8 +50,7 @@ public bool RetryEnabled [ForbidGUICalls] set { - Util.Invoke(this, () => - RetryCurrentActionButton.Visible = value); + Util.Invoke(this, () => RetryCurrentActionButton.Visible = value); } } @@ -62,10 +58,10 @@ public int ProgressValue { set { - Util.Invoke(this, () => - DialogProgressBar.Value = - Math.Max(DialogProgressBar.Minimum, - Math.Min(DialogProgressBar.Maximum, value))); + Util.Invoke(this, + () => DialogProgressBar.Value = Math.Max(DialogProgressBar.Minimum, + Math.Min(DialogProgressBar.Maximum, + value))); } } @@ -74,29 +70,71 @@ public bool ProgressIndeterminate [ForbidGUICalls] set { - Util.Invoke(this, () => - DialogProgressBar.Style = value - ? ProgressBarStyle.Marquee - : ProgressBarStyle.Continuous); + Util.Invoke(this, + () => DialogProgressBar.Style = value ? ProgressBarStyle.Marquee + : ProgressBarStyle.Continuous); } } #pragma warning restore IDE0027 - public void SetProgress(string label, long remaining, long total) + public void SetProgress(string label, + long remaining, long total) { + // download_size is allowed to be 0 if (total > 0) { Util.Invoke(this, () => { - if (progressBars.TryGetValue(label, out ProgressBar? pb)) + if (progressBars.TryGetValue(label, out LabeledProgressBar? pb)) { + var rateCounter = rateCounters[label]; + rateCounter.BytesLeft = remaining; + rateCounter.Size = total; + // download_size is allowed to be 0 - pb.Value = Math.Max(pb.Minimum, Math.Min(pb.Maximum, - (int) (100 * (total - remaining) / total))); + var newVal = Math.Max(pb.Minimum, + Math.Min(pb.Maximum, + (int)(100 * (total - remaining) / total))); + pb.Value = newVal; + pb.Text = rateCounter.Summary; + if (newVal >= 100) + { + rateCounter.Stop(); + for (int row = ProgressBarTable.GetPositionFromControl(pb).Row; row > 0; --row) + { + if (ProgressBarTable.GetControlFromPosition(1, row - 1) is LabeledProgressBar prevPb + && ProgressBarTable.GetControlFromPosition(0, row) is Label myLbl + && ProgressBarTable.GetControlFromPosition(0, row - 1) is Label prevLbl) + { + if (prevPb.Value >= 100) + { + // Previous row is completed, done + break; + } + else + { + // Previous row is in progress, swap + ProgressBarTable.SetRow(myLbl, row - 1); + ProgressBarTable.SetRow(pb, row - 1); + ProgressBarTable.SetRow(prevLbl, row); + ProgressBarTable.SetRow(prevPb, row); + } + } + } + } } else { + var rateCounter = new ByteRateCounter + { + BytesLeft = remaining, + Size = total + }; + rateCounters.Add(label, rateCounter); + rateCounter.Start(); + + var scrollToBottom = AtEnd(ProgressBarTable); var newLb = new Label() { AutoSize = true, @@ -104,34 +142,60 @@ public void SetProgress(string label, long remaining, long total) Margin = new Padding(0, 8, 0, 0), }; progressLabels.Add(label, newLb); - var newPb = new ProgressBar() + // download_size is allowed to be 0 + var newVal = Math.Max(0, + Math.Min(100, + (int)(100 * (total - remaining) / total))); + var newPb = new LabeledProgressBar() { Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, Minimum = 0, Maximum = 100, - // download_size is allowed to be 0 - Value = Math.Max(0, Math.Min(100, - (int) (100 * (total - remaining) / total))), + Value = newVal, Style = ProgressBarStyle.Continuous, + Text = rateCounter.Summary, }; progressBars.Add(label, newPb); + // Make room before adding + var newHeight = progressBars.Values + .Take(1) + .Concat(progressBars.Count == 1 + // If 1 row, show 1 + ? Enumerable.Empty() + // If >1 rows, show 1 + active + : progressBars.Values + .Where(pb => pb.Value < 100)) + .Sum(pb => pb.GetPreferredSize(Size.Empty).Height + + pb.Margin.Vertical); + if (ProgressBarTable.Height < newHeight) + { + VerticalSplitter.SplitterDistance = ProgressBarTable.Top + + newHeight + + ProgressBarTable.Margin.Vertical; + // Never show the horizontal scrollbar + ProgressBarTable.HorizontalScroll.Visible = false; + } + // Now add the new row + ProgressBarTable.Controls.Add(newLb, 0, -1); + ProgressBarTable.Controls.Add(newPb, 1, -1); + ProgressBarTable.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + ProgressBarTable.HorizontalScroll.Visible = false; + // If previously scrolled to the bottom, stay there, otherwise let user scroll up without interrupting + if (scrollToBottom) + { + ProgressBarTable.ScrollControlIntoView(newLb); + ProgressBarTable.ScrollControlIntoView(newPb); + } } - progressTimer.Start(); }); } } - /// - /// React to data received for a module, - /// adds or updates a label and progress bar so user can see how each download is going - /// - /// The module that is being downloaded - /// Number of bytes left to download - /// Number of bytes in complete download - public void SetModuleProgress(CkanModule module, long remaining, long total) - { - SetProgress(module.ToString(), remaining, total); - } + private static bool AtEnd(ScrollableControl control) + => control.DisplayRectangle.Height < control.Height + || control.DisplayRectangle.Height + + control.DisplayRectangle.Y + - control.VerticalScroll.LargeChange < control.Height; private Action? bgLogic; private Action? postLogic; @@ -152,85 +216,24 @@ private void RunWorkerCompleted(object? sender, RunWorkerCompletedEventArgs? e) postLogic?.Invoke(sender, e); } - private const int padding = 5; - private const int emptyHeight = 85; - - private readonly Dictionary progressLabels = new Dictionary(); - private readonly Dictionary progressBars = new Dictionary(); - private readonly Timer progressTimer = new Timer() { Interval = 3000 }; - - /// - /// Add new progress bars and remove completed ones (100%) in a single scheduled pass, - /// so they don't constantly flicker and jump around. - /// - private void ReflowProgressBars() - { - Util.Invoke(this, () => - { - foreach (var kvp in progressBars) - { - var lbl = progressLabels[kvp.Key]; - var pb = kvp.Value; - - if (pb.Value >= 100) - { - if (ProgressBarTable.Controls.Contains(pb)) - { - // Finished, remove in this pass - ProgressBarTable.Controls.Remove(lbl); - ProgressBarTable.Controls.Remove(pb); - ProgressBarTable.RowStyles.RemoveAt(0); - } - } - else if (!ProgressBarTable.Controls.Contains(pb)) - { - // Just started, add it in this pass - ProgressBarTable.Controls.Add(lbl, 0, -1); - ProgressBarTable.Controls.Add(pb, 1, -1); - ProgressBarTable.RowStyles.Add(new RowStyle(SizeType.AutoSize)); - } - } - - // Remove completed rows from our dicts - var removedKeys = progressBars - .Where(kvp => !ProgressBarTable.Controls.Contains(kvp.Value)) - .Select(kvp => kvp.Key) - .ToList(); - foreach (var key in removedKeys) - { - progressLabels.Remove(key); - progressBars.Remove(key); - } - - // Fit table to its contents (it assumes we will give it a size) - var cellPadding = ProgressBarTable.Padding.Vertical; - ProgressBarTable.Height = progressBars.Values - .Select(pb => pb.GetPreferredSize(Size.Empty).Height + cellPadding) - .Sum(); - TopPanel.Height = ProgressBarTable.Top + ProgressBarTable.Height + padding; - }); - } + private readonly int emptyHeight; - /// - /// React to completion of all downloads, - /// removes all the module progress bars since we don't need them anymore - /// - public void DownloadsComplete() - { - ClearProgressBars(); - progressTimer.Stop(); - } + private readonly Dictionary progressLabels = new Dictionary(); + private readonly Dictionary progressBars = new Dictionary(); + private readonly Dictionary rateCounters = new Dictionary(); private void ClearProgressBars() { - Util.Invoke(this, () => + ProgressBarTable.Controls.Clear(); + ProgressBarTable.RowStyles.Clear(); + progressLabels.Clear(); + progressBars.Clear(); + foreach (var rc in rateCounters.Values) { - ProgressBarTable.Controls.Clear(); - ProgressBarTable.RowStyles.Clear(); - progressLabels.Clear(); - progressBars.Clear(); - TopPanel.Height = emptyHeight; - }); + rc.Stop(); + } + rateCounters.Clear(); + VerticalSplitter.SplitterDistance = emptyHeight; } [ForbidGUICalls] @@ -244,7 +247,7 @@ public void Reset(bool cancelable) CancelCurrentActionButton.Visible = cancelable; CancelCurrentActionButton.Enabled = true; OkButton.Enabled = false; - MessageTextBox.Text = Properties.Resources.MainWaitPleaseWait; + DialogProgressBar.Text = Properties.Resources.MainWaitPleaseWait; }); } @@ -253,7 +256,7 @@ public void Finish() OnCancel = null; Util.Invoke(this, () => { - MessageTextBox.Text = Properties.Resources.MainWaitDone; + DialogProgressBar.Text = Properties.Resources.MainWaitDone; ProgressValue = 100; ProgressIndeterminate = false; CancelCurrentActionButton.Enabled = false; @@ -263,15 +266,14 @@ public void Finish() public void SetDescription(string message) { - Util.Invoke(this, () => - MessageTextBox.Text = "(" + message + ")"); + Util.Invoke(this, () => DialogProgressBar.Text = "(" + message + ")"); } public void SetMainProgress(string message, int percent) { Util.Invoke(this, () => { - MessageTextBox.Text = $"{message} - {percent}%"; + DialogProgressBar.Text = $"{message} - {percent}%"; ProgressIndeterminate = false; ProgressValue = percent; if (message != lastProgressMessage) @@ -282,16 +284,13 @@ public void SetMainProgress(string message, int percent) }); } - public void SetMainProgress(int percent, long bytesPerSecond, long bytesLeft) + public void SetMainProgress(ByteRateCounter rateCounter) { - var fullMsg = string.Format(CKAN.Properties.Resources.NetAsyncDownloaderProgress, - CkanModule.FmtSize(bytesPerSecond), - CkanModule.FmtSize(bytesLeft)); Util.Invoke(this, () => { - MessageTextBox.Text = $"{fullMsg} - {percent}%"; - ProgressIndeterminate = false; - ProgressValue = percent; + DialogProgressBar.Text = rateCounter.Summary; + ProgressIndeterminate = false; + ProgressValue = rateCounter.Percent; }); } @@ -300,8 +299,7 @@ public void SetMainProgress(int percent, long bytesPerSecond, long bytesLeft) [ForbidGUICalls] private void ClearLog() { - Util.Invoke(this, () => - LogTextBox.Text = ""); + Util.Invoke(this, () => LogTextBox.Text = ""); } public void AddLogMessage(string message) @@ -320,8 +318,7 @@ private void CancelCurrentActionButton_Click(object? sender, EventArgs? e) if (OnCancel != null) { OnCancel.Invoke(); - Util.Invoke(this, () => - CancelCurrentActionButton.Enabled = false); + Util.Invoke(this, () => CancelCurrentActionButton.Enabled = false); } } diff --git a/GUI/Enums/WindowExStyles.cs b/GUI/Enums/WindowExStyles.cs new file mode 100644 index 0000000000..9be922cf14 --- /dev/null +++ b/GUI/Enums/WindowExStyles.cs @@ -0,0 +1,15 @@ +using System; + +namespace CKAN.GUI +{ + // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles + [Flags] + public enum WindowExStyles : uint + { + WS_EX_TOPMOST = 0x8, + WS_EX_TRANSPARENT = 0x20, + WS_EX_TOOLWINDOW = 0x80, + WS_EX_CONTROLPARENT = 0x10000, + WS_EX_NOACTIVATE = 0x08000000, + } +} diff --git a/GUI/Enums/WindowStyles.cs b/GUI/Enums/WindowStyles.cs new file mode 100644 index 0000000000..e48aa708b2 --- /dev/null +++ b/GUI/Enums/WindowStyles.cs @@ -0,0 +1,13 @@ +using System; + +namespace CKAN.GUI +{ + // https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles + [Flags] + public enum WindowStyles : uint + { + WS_VISIBLE = 0x10000000, + WS_CHILD = 0x40000000, + WS_POPUP = 0x80000000, + } +} diff --git a/GUI/GUIUser.cs b/GUI/GUIUser.cs index dc8f519c20..8fb574bfe8 100644 --- a/GUI/GUIUser.cs +++ b/GUI/GUIUser.cs @@ -101,15 +101,14 @@ public void RaiseProgress(string message, int percent) } [ForbidGUICalls] - public void RaiseProgress(int percent, - long bytesPerSecond, long bytesLeft) + public void RaiseProgress(ByteRateCounter rateCounter) { Util.Invoke(main, () => { - wait.SetMainProgress(percent, bytesPerSecond, bytesLeft); + wait.SetMainProgress(rateCounter); statBarProgBar.Value = Math.Max(statBarProgBar.Minimum, - Math.Min(statBarProgBar.Maximum, percent)); + Math.Min(statBarProgBar.Maximum, rateCounter.Percent)); statBarProgBar.Style = ProgressBarStyle.Continuous; }); } diff --git a/GUI/Main/MainDownload.cs b/GUI/Main/MainDownload.cs index 938454d00d..4bd7e5c48c 100644 --- a/GUI/Main/MainDownload.cs +++ b/GUI/Main/MainDownload.cs @@ -47,12 +47,9 @@ private void CacheMod(object? sender, DoWorkEventArgs? e) && Manager?.Cache != null) { downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent); - downloader.Progress += Wait.SetModuleProgress; - downloader.AllComplete += Wait.DownloadsComplete; - downloader.StoreProgress += (module, remaining, total) => - Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, - module), - remaining, total); + downloader.DownloadProgress += OnModDownloading; + downloader.StoreProgress += OnModValidating; + downloader.OverallDownloadProgress += currentUser.RaiseProgress; Wait.OnCancel += downloader.CancelDownload; downloader.DownloadModules(new List { gm.ToCkanModule() }); e.Result = e.Argument; diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 5b03061502..5d5e45c99c 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -87,8 +87,12 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) var registry = registry_manager.registry; var installer = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser, userAgent); // Avoid accumulating multiple event handlers - installer.onReportModInstalled -= OnModInstalled; - installer.onReportModInstalled += OnModInstalled; + installer.OneComplete -= OnModInstalled; + installer.InstallProgress -= OnModInstalling; + installer.OneComplete += OnModInstalled; + installer.InstallProgress += OnModInstalling; + installer.RemoveProgress -= OnModRemoving; + installer.RemoveProgress += OnModRemoving; // this will be the final list of mods we want to install var toInstall = new List(); @@ -191,12 +195,9 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) }); tabController.SetTabLock(true); - IDownloader downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent); - downloader.Progress += Wait.SetModuleProgress; - downloader.AllComplete += Wait.DownloadsComplete; - downloader.StoreProgress += (module, remaining, total) => - Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, module), - remaining, total); + var downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent); + downloader.DownloadProgress += OnModDownloading; + downloader.StoreProgress += OnModValidating; Wait.OnCancel += () => { @@ -230,7 +231,7 @@ private void InstallMods(object? sender, DoWorkEventArgs? e) } if (!canceled && toUpgrade.Count > 0) { - installer.Upgrade(toUpgrade, downloader, ref possibleConfigOnlyDirs, registry_manager, true, true, false); + installer.Upgrade(toUpgrade, downloader, ref possibleConfigOnlyDirs, registry_manager, true, false); toUpgrade.Clear(); } if (canceled) @@ -375,10 +376,54 @@ private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet? po } } + /// + /// React to data received for a module + /// + /// The module that is being downloaded + /// Number of bytes left to download + /// Number of bytes in complete download + public void OnModDownloading(CkanModule mod, long remaining, long total) + { + if (total > 0) + { + Wait.SetProgress(string.Format(Properties.Resources.Downloading, + mod.name), + remaining, total); + } + } + + private void OnModValidating(CkanModule mod, long remaining, long total) + { + if (total > 0) + { + Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload, + mod.name), + remaining, total); + } + } + + private void OnModInstalling(CkanModule mod, long remaining, long total) + { + if (total > 0) + { + Wait.SetProgress(string.Format(Properties.Resources.MainInstallInstallingMod, + mod.name), + remaining, total); + } + } + + private void OnModRemoving(InstalledModule instMod, long remaining, long total) + { + if (total > 0) + { + Wait.SetProgress(string.Format(Properties.Resources.MainInstallRemovingMod, + instMod.Module.name), + remaining, total); + } + } + private void OnModInstalled(CkanModule mod) { - currentUser.RaiseMessage(Properties.Resources.MainInstallModSuccess, - mod); LabelsAfterInstall(mod); } @@ -491,7 +536,7 @@ private void PostInstallMods(object? sender, RunWorkerCompletedEventArgs? e) break; default: - currentUser.RaiseMessage("{0}", e.Error.Message); + currentUser.RaiseMessage("{0}", e.Error.ToString()); break; } diff --git a/GUI/Main/MainRepo.cs b/GUI/Main/MainRepo.cs index 2dae6822fc..3a4254f6cc 100644 --- a/GUI/Main/MainRepo.cs +++ b/GUI/Main/MainRepo.cs @@ -91,11 +91,11 @@ private void UpdateRepo(object? sender, DoWorkEventArgs? e) { bool canceled = false; var downloader = new NetAsyncDownloader(currentUser, () => null, userAgent); - downloader.Progress += (target, remaining, total) => + downloader.TargetProgress += (target, remaining, total) => { var repo = repos.Where(r => target.urls.Contains(r.uri)) .FirstOrDefault(); - if (repo != null) + if (repo != null && total > 0) { Wait.SetProgress(repo.name, remaining, total); } diff --git a/GUI/Main/MainWait.cs b/GUI/Main/MainWait.cs index 31537bdadb..b91c9b557b 100644 --- a/GUI/Main/MainWait.cs +++ b/GUI/Main/MainWait.cs @@ -51,13 +51,10 @@ public void HideWaitDialog() /// Message displayed above the DialogProgress bar public void FailWaitDialog(string statusMsg, string logMsg, string description) { - Util.Invoke(statusStrip1, () => + Util.Invoke(this, () => { StatusProgress.Visible = false; currentUser.RaiseMessage("{0}", statusMsg); - }); - Util.Invoke(WaitTabPage, () => - { RecreateDialogs(); Wait.Finish(); }); diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 5d5091b295..ba5741ecc1 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -222,6 +222,8 @@ Are you sure you want to install them? Cancelling will abort the entire installa Mods (*.zip)|*.zip Status log Status log + Installing {0} + Removing {0} {0} requires {1} but it is not listed in the index, or not available for your game version. Module {0} required but it is not listed in the index, or not available for your game version. Bad metadata detected for module {0}: {1} @@ -299,6 +301,7 @@ If you suspect a bug in the client: https://github.com/KSP-CKAN/CKAN/issues/new/ Steam store: Ctrl-click or Shift-click to make sticky Download failed! + Downloading {0} Validating {0} Loading modules Loading registry... diff --git a/Netkan/ConsoleUser.cs b/Netkan/ConsoleUser.cs index d4ac8c8807..85f6285e93 100644 --- a/Netkan/ConsoleUser.cs +++ b/Netkan/ConsoleUser.cs @@ -276,18 +276,15 @@ public void RaiseProgress(string message, int percent) Console.Write("\r\n{0}", message); } - public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) + public void RaiseProgress(ByteRateCounter rateCounter) { // In headless mode, only print a new message if the percent has changed, // to reduce clutter in logs for large downloads - if (!Headless || percent != previousPercent) + if (!Headless || rateCounter.Percent != previousPercent) { - var fullMsg = string.Format(Properties.Resources.NetAsyncDownloaderProgress, - CkanModule.FmtSize(bytesPerSecond), - CkanModule.FmtSize(bytesLeft)); - // The \r at the front here causes download messages to *overwrite* each other. - Console.Write("\r{0} - {1}% ", fullMsg, percent); - previousPercent = percent; + // The \r at the start causes download messages to *overwrite* each other. + Console.Write("\r{0} ", rateCounter.Summary); + previousPercent = rateCounter.Percent; } } diff --git a/Tests/CapturingUser.cs b/Tests/CapturingUser.cs index bc80b44432..fdf304b5b7 100644 --- a/Tests/CapturingUser.cs +++ b/Tests/CapturingUser.cs @@ -40,10 +40,10 @@ public void RaiseProgress(string message, int percent) RaisedProgresses.Add(new Tuple(message, percent)); } - public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) + public void RaiseProgress(ByteRateCounter rateCounter) { - RaisedProgresses.Add(new Tuple($"{bytesPerSecond} {bytesLeft}", - percent)); + RaisedProgresses.Add(new Tuple($"{rateCounter.BytesPerSecond} {rateCounter.BytesLeft}", + rateCounter.Percent)); } public void RaiseMessage(string message, params object[] args) diff --git a/Tests/Core/ModuleInstallerDirTest.cs b/Tests/Core/ModuleInstallerDirTest.cs index 355f0dc86b..ac772cf992 100644 --- a/Tests/Core/ModuleInstallerDirTest.cs +++ b/Tests/Core/ModuleInstallerDirTest.cs @@ -58,7 +58,7 @@ public void SetUp() _gameDir = _instance.KSP.GameDir(); _gameDataDir = _instance.KSP.game.PrimaryModDirectory(_instance.KSP); var testModFile = TestData.DogeCoinFlagZip(); - _manager.Cache?.Store(_testModule!, testModFile, new Progress(percent => {})); + _manager.Cache?.Store(_testModule!, testModFile, new Progress(bytes => {})); HashSet? possibleConfigOnlyDirs = null; _installer.InstallList( new List() { _testModule! }, diff --git a/Tests/Core/ModuleInstallerTests.cs b/Tests/Core/ModuleInstallerTests.cs index 6da9fcc1ea..3c4760244e 100644 --- a/Tests/Core/ModuleInstallerTests.cs +++ b/Tests/Core/ModuleInstallerTests.cs @@ -406,7 +406,7 @@ public void CanInstallMod() var cache_path = manager.Cache?.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(percent => {})); + new Progress(bytes => {})); Assert.IsTrue(manager.Cache?.IsCached(TestData.DogeCoinFlag_101_module())); Assert.IsTrue(File.Exists(cache_path)); @@ -455,7 +455,7 @@ public void CanUninstallMod() registry.RepositoriesAdd(repo.repo); manager.Cache?.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(percent => {})); + new Progress(bytes => {})); var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -503,7 +503,7 @@ public void UninstallEmptyDirs() registry.RepositoriesAdd(repo.repo); manager.Cache?.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(percent => {})); + new Progress(bytes => {})); var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -519,7 +519,7 @@ public void UninstallEmptyDirs() // Install the plugin test mod. manager.Cache?.Store(TestData.DogeCoinPlugin_module(), TestData.DogeCoinPluginZip(), - new Progress(percent => {})); + new Progress(bytes => {})); modules.Add(TestData.DogeCoinPlugin_module()); @@ -711,7 +711,7 @@ public void ModuleManagerInstancesAreDecoupled() // Copy the zip file to the cache directory. manager.Cache?.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(percent => {})); + new Progress(bytes => {})); // Attempt to install it. var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -970,7 +970,7 @@ public void InstallList_RealZipSlip_Throws() // Copy the zip file to the cache directory. manager.Cache?.Store(TestData.DogeCoinFlag_101ZipSlip_module(), TestData.DogeCoinFlagZipSlipZip(), - new Progress(percent => {})); + new Progress(bytes => {})); // Attempt to install it. var modules = new List { TestData.DogeCoinFlag_101ZipSlip_module() }; @@ -1012,7 +1012,7 @@ public void InstallList_RealZipBomb_DoesNotThrow() // Copy the zip file to the cache directory. manager.Cache?.Store(TestData.DogeCoinFlag_101ZipBomb_module(), TestData.DogeCoinFlagZipBombZip(), - new Progress(percent => {})); + new Progress(bytes => {})); // Attempt to install it. var modules = new List { TestData.DogeCoinFlag_101ZipBomb_module() }; @@ -1081,7 +1081,7 @@ public void Replace_WithCompatibleModule_Succeeds() // Act registry.RegisterModule(replaced, new List(), inst.KSP, false); - manager.Cache?.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(percent => {})); + manager.Cache?.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); var replacement = querier.GetReplacement(replaced.identifier, new GameVersionCriteria(new GameVersion(1, 12)))!; installer.Replace(Enumerable.Repeat(replacement, 1), @@ -1149,7 +1149,7 @@ public void Replace_WithIncompatibleModule_Fails() // Act registry.RegisterModule(replaced, new List(), inst.KSP, false); - manager.Cache?.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(percent => {})); + manager.Cache?.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); var replacement = querier.GetReplacement(replaced.identifier, new GameVersionCriteria(new GameVersion(1, 11))); @@ -1307,7 +1307,7 @@ public void Upgrade_WithAutoInst_RemovesAutoRemovable(string[] regularMods, var module = CkanModule.FromJson(m); manager.Cache?.Store(module, TestData.DogeCoinFlagZip(), - new Progress(percent => {})); + new Progress(bytes => {})); if (!querier.IsInstalled(module.identifier, false)) { registry.RegisterModule(module, @@ -1381,7 +1381,7 @@ private void installTestPlugin(string unmanaged, string moduleJson, string zipPa File.WriteAllText(inst.KSP.ToAbsoluteGameDir(unmanaged), "Not really a DLL, are we?"); regMgr.ScanUnmanagedFiles(); - manager.Cache?.Store(module, zipPath, new Progress(percent => {})); + manager.Cache?.Store(module, zipPath, new Progress(bytes => {})); // Act HashSet? possibleConfigOnlyDirs = null; diff --git a/Tests/Core/Net/NetModuleCacheTests.cs b/Tests/Core/Net/NetModuleCacheTests.cs index 67c8ff866f..9f0138b24d 100644 --- a/Tests/Core/Net/NetModuleCacheTests.cs +++ b/Tests/Core/Net/NetModuleCacheTests.cs @@ -50,14 +50,14 @@ public void StoreInvalid() Assert.Throws(() => module_cache?.Store( TestData.DogeCoinFlag_101_LZMA_module, - "/DoesNotExist.zip", new Progress(percent => {}))); + "/DoesNotExist.zip", new Progress(bytes => {}))); // Try to store the LZMA-format DogeCoin zip into a NetModuleCache // and expect an InvalidModuleFileKraken Assert.Throws(() => module_cache?.Store( TestData.DogeCoinFlag_101_LZMA_module, - TestData.DogeCoinFlagZipLZMA, new Progress(percent => {}))); + TestData.DogeCoinFlagZipLZMA, new Progress(bytes => {}))); // Try to store the normal DogeCoin zip into a NetModuleCache // using the WRONG metadata (file size and hashes) @@ -65,7 +65,7 @@ public void StoreInvalid() Assert.Throws(() => module_cache?.Store( TestData.DogeCoinFlag_101_LZMA_module, - TestData.DogeCoinFlagZip(), new Progress(percent => {}))); + TestData.DogeCoinFlagZip(), new Progress(bytes => {}))); } [Test] diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index a409a09183..8553f7c4fd 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -166,7 +166,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() // Act // Install module and set it as pre-installed - manager.Cache?.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), new Progress(percent => {})); + manager.Cache?.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), new Progress(bytes => {})); registry.RegisterModule(anyVersionModule, new List(), instance.KSP, false); HashSet? possibleConfigOnlyDirs = null;