From 35f0ae6afbb32a1f0ce54fca1f648fa542baaeb3 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Mon, 11 Mar 2024 14:12:10 -0500 Subject: [PATCH] Proportional, granular progress updates for installing --- ConsoleUI/InstallScreen.cs | 8 +- Core/Extensions/IOExtensions.cs | 6 + Core/ModuleInstaller.cs | 311 +++++++++++------- Core/Registry/Registry.cs | 8 +- .../ProgressFilesOffsetsToPercent.cs | 18 +- .../ProgressScalePercentsByFileSize.cs | 7 +- Tests/CmdLine/UpgradeTests.cs | 4 +- Tests/Core/ModuleInstallerTests.cs | 18 +- Tests/Core/Registry/Registry.cs | 19 +- Tests/Core/Registry/RegistryManager.cs | 2 +- .../Relationships/RelationshipResolver.cs | 6 +- Tests/GUI/Model/GUIMod.cs | 2 +- Tests/GUI/Model/ModList.cs | 2 +- 13 files changed, 243 insertions(+), 168 deletions(-) diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index 6816819f0a..d7eb965ae6 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -65,10 +65,8 @@ public override void Run(ConsoleTheme theme, Action process = null HashSet possibleConfigOnlyDirs = null; - ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this) - { - onReportModInstalled = OnModInstalled - }; + ModuleInstaller inst = new ModuleInstaller(manager.CurrentInstance, manager.Cache, this); + inst.onReportModInstalled += OnModInstalled; if (plan.Remove.Count > 0) { inst.UninstallList(plan.Remove, ref possibleConfigOnlyDirs, regMgr, true, new List(plan.Install)); plan.Remove.Clear(); @@ -103,9 +101,9 @@ public override void Run(ConsoleTheme theme, Action process = null } trans.Complete(); + inst.onReportModInstalled -= OnModInstalled; // Don't let the installer re-use old screen references inst.User = null; - inst.onReportModInstalled = null; } catch (CancelledActionKraken) { // Don't need to tell the user they just cancelled out. diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs index 5f2ec9f6ef..cd477e412b 100644 --- a/Core/Extensions/IOExtensions.cs +++ b/Core/Extensions/IOExtensions.cs @@ -115,6 +115,12 @@ public static void CopyTo(this Stream src, lastProgressTime = now; } } + if (timer != null) + { + timer.Stop(); + timer.Close(); + timer = null; + } // Make sure we get a final progress notification after we're done progress.Report(total); } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 6a0b79865a..ed86ba4a89 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -3,7 +3,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using System.Transactions; using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.Zip; @@ -18,35 +17,35 @@ namespace CKAN { - public delegate void ModuleInstallerReportModInstalled(CkanModule module); - public struct InstallableFile { public ZipEntry source; - public string destination; - public bool makedir; + public string destination; + public bool makedir; } public class ModuleInstaller { public IUser User { get; set; } + public event Action onReportModInstalled = null; + private static readonly ILog log = LogManager.GetLogger(typeof(ModuleInstaller)); - private readonly GameInstance ksp; + private readonly GameInstance instance; private readonly NetModuleCache Cache; - public ModuleInstallerReportModInstalled onReportModInstalled = null; - // Constructor - public ModuleInstaller(GameInstance ksp, NetModuleCache cache, IUser user) + public ModuleInstaller(GameInstance inst, NetModuleCache cache, IUser user) { - User = user; - Cache = cache; - this.ksp = ksp; - log.DebugFormat("Creating ModuleInstaller for {0}", ksp.GameDir()); + User = user; + Cache = cache; + instance = inst; + log.DebugFormat("Creating ModuleInstaller for {0}", instance.GameDir()); } + #region Downloading + /// /// Downloads the given mod to the cache. Returns the filename it was saved to. /// @@ -80,9 +79,7 @@ public static string Download(CkanModule module, string filename, NetModuleCache /// Checks the CKAN cache first. /// public string CachedOrDownload(CkanModule module, string filename = null) - { - return CachedOrDownload(module, Cache, filename); - } + => CachedOrDownload(module, Cache, filename); /// /// Returns the path to a cached copy of a module if it exists, or downloads @@ -108,6 +105,23 @@ public static string CachedOrDownload(CkanModule module, NetModuleCache cache, s return full_path; } + /// + /// Makes sure all the specified mods are downloaded. + /// + private void DownloadModules(IEnumerable mods, IDownloader downloader) + { + List downloads = mods.Where(module => !Cache.IsCached(module)).ToList(); + + if (downloads.Count > 0) + { + downloader.DownloadModules(downloads); + } + } + + #endregion + + #region Installation + /// /// Installs all modules given a list of identifiers as a transaction. Resolves dependencies. /// This *will* save the registry at the end of operation. @@ -129,12 +143,14 @@ public void InstallList(ICollection modules, User.RaiseProgress(Properties.Resources.ModuleInstallerNothingToInstall, 100); return; } - var resolver = new RelationshipResolver(modules, null, options, registry_manager.registry, ksp.VersionCriteria()); + var resolver = new RelationshipResolver(modules, null, options, + registry_manager.registry, + instance.VersionCriteria()); var modsToInstall = resolver.ModList().ToList(); - List downloads = new List(); + var downloads = new List(); // Make sure we have enough space to install this stuff - CKANPathUtils.CheckFreeSpace(new DirectoryInfo(ksp.GameDir()), + CKANPathUtils.CheckFreeSpace(new DirectoryInfo(instance.GameDir()), modsToInstall.Select(m => m.install_size) .Sum(), Properties.Resources.NotEnoughSpaceToInstall); @@ -145,7 +161,7 @@ public void InstallList(ICollection modules, User.RaiseMessage(Properties.Resources.ModuleInstallerAboutToInstall); User.RaiseMessage(""); - foreach (CkanModule module in modsToInstall) + foreach (var module in modsToInstall) { User.RaiseMessage(" * {0}", Cache.DescribeAvailability(module)); if (!Cache.IsMaybeCachedZip(module)) @@ -165,74 +181,50 @@ public void InstallList(ICollection modules, { downloader = new NetAsyncModulesDownloader(User, Cache); } - downloader.DownloadModules(downloads); } // 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(ksp.GameDir()), + CKANPathUtils.CheckFreeSpace(new DirectoryInfo(instance.GameDir()), modsToInstall.Select(m => m.install_size) .Sum(), Properties.Resources.NotEnoughSpaceToInstall); // We're about to install all our mods; so begin our transaction. - using (TransactionScope transaction = CkanTransaction.CreateTransactionScope()) + using (var transaction = CkanTransaction.CreateTransactionScope()) { + CkanModule installing = null; + var progress = new ProgressScalePercentsByFileSizes( + new Progress(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++) { - // The post-install steps start at 70%, so count up to 60% for installation - int percent_complete = (i * 60) / modsToInstall.Count; - - User.RaiseProgress(string.Format(Properties.Resources.ModuleInstallerInstallingMod, modsToInstall[i]), - percent_complete); - - Install(modsToInstall[i], resolver.IsAutoInstalled(modsToInstall[i]), registry_manager.registry, ref possibleConfigOnlyDirs); + installing = modsToInstall[i]; + Install(installing, + resolver.IsAutoInstalled(installing), + registry_manager.registry, + ref possibleConfigOnlyDirs, + progress); + progress.NextFile(); } - User.RaiseProgress(Properties.Resources.ModuleInstallerUpdatingRegistry, 70); - + User.RaiseProgress(Properties.Resources.ModuleInstallerUpdatingRegistry, 90); registry_manager.Save(!options.without_enforce_consistency); - User.RaiseProgress(Properties.Resources.ModuleInstallerCommitting, 80); - + User.RaiseProgress(Properties.Resources.ModuleInstallerCommitting, 95); transaction.Complete(); - } EnforceCacheSizeLimit(registry_manager.registry); - User.RaiseProgress(Properties.Resources.ModuleInstallerDone, 100); } - /// - /// Returns the module contents if and only if we have it - /// available in our cache. Returns null, otherwise. - /// - /// Intended for previews. - /// - public IEnumerable GetModuleContentsList(CkanModule module) - { - string filename = Cache.GetCachedFilename(module); - - if (filename == null) - { - return Enumerable.Empty(); - } - - try - { - return FindInstallableFiles(module, filename, ksp) - // Skip folders - .Where(f => !f.source.IsDirectory) - .Select(f => ksp.ToRelativeGameDir(f.destination)); - } - catch (ZipException) - { - return Enumerable.Empty(); - } - } - /// /// Install our mod from the filename supplied. /// If no file is supplied, we will check the cache or throw FileNotFoundKraken. @@ -246,16 +238,21 @@ public IEnumerable GetModuleContentsList(CkanModule module) /// /// TODO: The name of this and InstallModule() need to be made more distinctive. /// - private void Install(CkanModule module, bool autoInstalled, Registry registry, ref HashSet possibleConfigOnlyDirs, string filename = null) + private void Install(CkanModule module, + bool autoInstalled, + Registry registry, + ref HashSet possibleConfigOnlyDirs, + IProgress progress, + string filename = null) { CheckKindInstallationKraken(module); - - ModuleVersion version = registry.InstalledVersion(module.identifier); + var version = registry.InstalledVersion(module.identifier); // TODO: This really should be handled by higher-up code. if (version != null && !(version is UnmanagedModuleVersion)) { - User.RaiseMessage(Properties.Resources.ModuleInstallerAlreadyInstalled, module.identifier, version); + User.RaiseMessage(Properties.Resources.ModuleInstallerAlreadyInstalled, + module.identifier, version); return; } @@ -265,19 +262,19 @@ private void Install(CkanModule module, bool autoInstalled, Registry registry, r // If we *still* don't have a file, then kraken bitterly. if (filename == null) { - throw new FileNotFoundKraken( - null, - string.Format(Properties.Resources.ModuleInstallerZIPNotInCache, module) - ); + throw new FileNotFoundKraken(null, + string.Format(Properties.Resources.ModuleInstallerZIPNotInCache, + module)); } using (var transaction = CkanTransaction.CreateTransactionScope()) { // Install all the things! - IEnumerable files = InstallModule(module, filename, registry, ref possibleConfigOnlyDirs); + var files = InstallModule(module, filename, registry, + ref possibleConfigOnlyDirs, progress); // Register our module and its files. - registry.RegisterModule(module, files, ksp, autoInstalled); + registry.RegisterModule(module, files, instance, autoInstalled); // Finish our transaction, but *don't* save the registry; we may be in an // intermediate, inconsistent state. @@ -315,7 +312,11 @@ private static void CheckKindInstallationKraken(CkanModule module) /// Propagates a CancelledActionKraken if the user decides not to overwite unowned files. /// Propagates a FileExistsKraken if we were going to overwrite a file. /// - private IEnumerable InstallModule(CkanModule module, string zip_filename, Registry registry, ref HashSet possibleConfigOnlyDirs) + private List InstallModule(CkanModule module, + string zip_filename, + Registry registry, + ref HashSet possibleConfigOnlyDirs, + IProgress moduleProgress) { CheckKindInstallationKraken(module); var createdPaths = new List(); @@ -323,9 +324,9 @@ private IEnumerable InstallModule(CkanModule module, string zip_filename using (ZipFile zipfile = new ZipFile(zip_filename)) { var filters = ServiceLocator.Container.Resolve().GlobalInstallFilters - .Concat(ksp.InstallFilters) + .Concat(instance.InstallFilters) .ToHashSet(); - var files = FindInstallableFiles(module, zipfile, ksp) + var files = FindInstallableFiles(module, zipfile, instance) .Where(instF => !filters.Any(filt => instF.destination.Contains(filt)) // Skip the file if it's a ckan file, these should never be copied to GameData @@ -341,8 +342,8 @@ private IEnumerable InstallModule(CkanModule module, string zip_filename // Find where we're installing identifier.optionalversion.dll // (file name might not be an exact match with manually installed) var dllFolders = files - .Select(f => ksp.ToRelativeGameDir(f.destination)) - .Where(relPath => ksp.DllPathToIdentifier(relPath) == module.identifier) + .Select(f => instance.ToRelativeGameDir(f.destination)) + .Where(relPath => instance.DllPathToIdentifier(relPath) == module.identifier) .Select(relPath => Path.GetDirectoryName(relPath)) .ToHashSet(); // Make sure that the DLL is actually included in the install @@ -357,7 +358,7 @@ private IEnumerable InstallModule(CkanModule module, string zip_filename Properties.Resources.ModuleInstallerBadDLLLocation, module.identifier, dll)); } // Delete the manually installed DLL transaction-style because we believe we'll be replacing it - var toDelete = ksp.ToAbsoluteGameDir(dll); + var toDelete = instance.ToAbsoluteGameDir(dll); log.DebugFormat("Deleting manually installed DLL {0}", toDelete); TxFileManager file_transaction = new TxFileManager(); file_transaction.Snapshot(toDelete); @@ -374,7 +375,7 @@ private IEnumerable InstallModule(CkanModule module, string zip_filename var fileMsg = conflicting .OrderBy(c => c.Value) .Aggregate("", (a, b) => - $"{a}\r\n- {ksp.ToRelativeGameDir(b.Key.destination)} ({(b.Value ? Properties.Resources.ModuleInstallerFileSame : Properties.Resources.ModuleInstallerFileDifferent)})"); + $"{a}\r\n- {instance.ToRelativeGameDir(b.Key.destination)} ({(b.Value ? Properties.Resources.ModuleInstallerFileSame : Properties.Resources.ModuleInstallerFileDifferent)})"); if (User.RaiseYesNoDialog(string.Format( Properties.Resources.ModuleInstallerOverwrite, module.name, fileMsg))) { @@ -387,13 +388,23 @@ private IEnumerable InstallModule(CkanModule module, string zip_filename } } } + var fileProgress = moduleProgress != null + ? new ProgressFilesOffsetsToPercent(moduleProgress, + files.Select(f => f.source.Size)) + : null; foreach (InstallableFile file in files) { log.DebugFormat("Copying {0}", file.source.Name); - createdPaths.Add(CopyZipEntry(zipfile, file.source, file.destination, file.makedir)); - if (file.source.IsDirectory && possibleConfigOnlyDirs != null) + var path = CopyZipEntry(zipfile, file.source, file.destination, file.makedir, + fileProgress); + fileProgress?.NextFile(); + if (path != null) { - possibleConfigOnlyDirs.Remove(file.destination); + createdPaths.Add(path); + if (file.source.IsDirectory && possibleConfigOnlyDirs != null) + { + possibleConfigOnlyDirs.Remove(file.destination); + } } } log.InfoFormat("Installed {0}", module); @@ -401,15 +412,17 @@ private IEnumerable InstallModule(CkanModule module, string zip_filename catch (FileExistsKraken kraken) { // Decorate the kraken with our module and re-throw - kraken.filename = ksp.ToRelativeGameDir(kraken.filename); + kraken.filename = instance.ToRelativeGameDir(kraken.filename); kraken.installingModule = module; kraken.owningModule = registry.FileOwner(kraken.filename); throw; } } - return createdPaths.Where(p => p != null); + return createdPaths; } + #region File overwrites + /// /// Find files in the given list that are already installed and unowned. /// Note, this compares files on demand; Memoize for performance! @@ -424,7 +437,7 @@ private IEnumerable> FindConflictingFiles(Zi { if (!file.source.IsDirectory && File.Exists(file.destination) - && registry.FileOwner(ksp.ToRelativeGameDir(file.destination)) == null) + && registry.FileOwner(instance.ToRelativeGameDir(file.destination)) == null) { log.DebugFormat("Comparing {0}", file.destination); using (Stream zipStream = zip.GetInputStream(file.source)) @@ -499,6 +512,10 @@ private void DeleteConflictingFiles(IEnumerable files) } } + #endregion + + #region Find files + /// /// Given a module and an open zipfile, return all the files that would be installed /// for this module. @@ -560,6 +577,36 @@ public static List FindInstallableFiles(CkanModule module, stri } } + /// + /// Returns the module contents if and only if we have it + /// available in our cache. Returns null, otherwise. + /// + /// Intended for previews. + /// + public IEnumerable GetModuleContentsList(CkanModule module) + { + string filename = Cache.GetCachedFilename(module); + + if (filename == null) + { + return Enumerable.Empty(); + } + + try + { + return FindInstallableFiles(module, filename, instance) + // Skip folders + .Where(f => !f.source.IsDirectory) + .Select(f => instance.ToRelativeGameDir(f.destination)); + } + catch (ZipException) + { + return Enumerable.Empty(); + } + } + + #endregion + /// /// Copy the entry from the opened zipfile to the path specified. /// @@ -567,9 +614,13 @@ public static List FindInstallableFiles(CkanModule module, stri /// Path of file or directory that was created. /// May differ from the input fullPath! /// - internal static string CopyZipEntry(ZipFile zipfile, ZipEntry entry, string fullPath, bool makeDirs) + internal static string CopyZipEntry(ZipFile zipfile, + ZipEntry entry, + string fullPath, + bool makeDirs, + IProgress progress) { - TxFileManager file_transaction = new TxFileManager(); + var file_transaction = new TxFileManager(); if (entry.IsDirectory) { @@ -581,8 +632,9 @@ internal static string CopyZipEntry(ZipFile zipfile, ZipEntry entry, string full } // Windows silently trims trailing spaces, get the path it will actually use - fullPath = CKANPathUtils.NormalizePath(Path.GetDirectoryName( - Path.Combine(fullPath, "DUMMY"))); + fullPath = CKANPathUtils.NormalizePath( + Path.GetDirectoryName( + Path.Combine(fullPath, "DUMMY"))); log.DebugFormat("Making directory '{0}'", fullPath); file_transaction.CreateDirectory(fullPath); @@ -613,14 +665,22 @@ internal static string CopyZipEntry(ZipFile zipfile, ZipEntry entry, string full try { // It's a file! Prepare the streams - using (Stream zipStream = zipfile.GetInputStream(entry)) - using (FileStream writer = File.Create(fullPath)) + using (var zipStream = zipfile.GetInputStream(entry)) + using (var writer = File.Create(fullPath)) { // Windows silently changes paths ending with spaces, get the name it actually used fullPath = CKANPathUtils.NormalizePath(writer.Name); // 4k is the block size on practically every disk and OS. byte[] buffer = new byte[4096]; - StreamUtils.Copy(zipStream, writer, buffer); + progress?.Report(0); + StreamUtils.Copy(zipStream, writer, buffer, + // This doesn't fire at all if the interval never elapses + (sender, e) => + { + progress?.Report(e.Processed); + }, + UnzipProgressInterval, + entry, "CopyZipEntry"); } } catch (DirectoryNotFoundException ex) @@ -633,6 +693,12 @@ internal static string CopyZipEntry(ZipFile zipfile, ZipEntry entry, string full return fullPath; } + private static readonly TimeSpan UnzipProgressInterval = TimeSpan.FromMilliseconds(200); + + #endregion + + #region Uninstallation + /// /// Uninstalls all the mods provided, including things which depend upon them. /// This *DOES* save the registry. @@ -672,7 +738,7 @@ public void UninstallList( .Where(im => !revdep.Contains(im.identifier)) .Concat(installing?.Select(m => new InstalledModule(null, m, Array.Empty(), false)) ?? Array.Empty()) .ToList(), - ksp.VersionCriteria()) + instance.VersionCriteria()) .Select(im => im.identifier)) .ToList(); @@ -752,7 +818,7 @@ private void Uninstall(string identifier, ref HashSet possibleConfigOnly foreach (string relPath in modFiles) { - string absPath = ksp.ToAbsoluteGameDir(relPath); + string absPath = instance.ToAbsoluteGameDir(relPath); try { @@ -806,7 +872,7 @@ private void Uninstall(string identifier, ref HashSet possibleConfigOnly } // Remove from registry. - registry.DeregisterModule(ksp, identifier); + registry.DeregisterModule(instance, identifier); // Our collection of directories may leave empty parent directories. directoriesToDelete = AddParentDirectories(directoriesToDelete); @@ -819,25 +885,25 @@ private void Uninstall(string identifier, ref HashSet possibleConfigOnly // It is bad if any of this directories gets removed // So we protect them // A few string comparisons will be cheaper than hitting the disk, so do this first - if (ksp.game.IsReservedDirectory(ksp, directory)) + if (instance.game.IsReservedDirectory(instance, directory)) { log.DebugFormat("Directory {0} is reserved, skipping", directory); continue; } // See what's left in this folder and what we can do about it - GroupFilesByRemovable(ksp.ToRelativeGameDir(directory), - registry, modFiles, ksp.game, + GroupFilesByRemovable(instance.ToRelativeGameDir(directory), + registry, modFiles, instance.game, (Directory.Exists(directory) ? Directory.EnumerateFileSystemEntries(directory, "*", SearchOption.AllDirectories) : Enumerable.Empty()) - .Select(f => ksp.ToRelativeGameDir(f)) + .Select(f => instance.ToRelativeGameDir(f)) .ToArray(), out string[] removable, out string[] notRemovable); // Delete the auto-removable files and dirs - foreach (var absPath in removable.Select(ksp.ToAbsoluteGameDir)) + foreach (var absPath in removable.Select(instance.ToAbsoluteGameDir)) { if (File.Exists(absPath)) { @@ -873,7 +939,7 @@ private void Uninstall(string identifier, ref HashSet possibleConfigOnly log.DebugFormat("Removing {0}", directory); Directory.Delete(directory); } - else if (notRemovable.Except(possibleConfigOnlyDirs?.Select(ksp.ToRelativeGameDir) + else if (notRemovable.Except(possibleConfigOnlyDirs?.Select(instance.ToRelativeGameDir) ?? Enumerable.Empty()) // Can't remove if owned by some other mod .Any(relPath => registry.FileOwner(relPath) != null @@ -944,7 +1010,7 @@ public HashSet AddParentDirectories(HashSet directories) return new HashSet(Platform.PathComparer); } - var gameDir = CKANPathUtils.NormalizePath(ksp.GameDir()); + var gameDir = CKANPathUtils.NormalizePath(instance.GameDir()); return directories .Where(dir => !string.IsNullOrWhiteSpace(dir)) // Normalize all paths before deduplicate @@ -987,10 +1053,12 @@ public HashSet AddParentDirectories(HashSet directories) return results; }) - .Where(dir => !ksp.game.IsReservedDirectory(ksp, dir)) + .Where(dir => !instance.game.IsReservedDirectory(instance, dir)) .ToHashSet(); } + #endregion + #region AddRemove /// @@ -1032,7 +1100,11 @@ private void AddRemove(ref HashSet possibleConfigOnlyDirs, RegistryManag percent_complete); // 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 ?? newModulesAreAutoInstalled, registry_manager.registry, ref possibleConfigOnlyDirs); + Install(module, + previous?.AutoInstalled ?? newModulesAreAutoInstalled, + registry_manager.registry, + ref possibleConfigOnlyDirs, + null); } User.RaiseProgress(Properties.Resources.ModuleInstallerUpdatingRegistry, 80); @@ -1068,7 +1140,7 @@ public void Upgrade(IEnumerable modules, modules.Select(m => registry.InstalledModule(m.identifier)?.Module).Where(m => m != null), RelationshipResolverOptions.DependsOnlyOpts(), registry, - ksp.VersionCriteria() + instance.VersionCriteria() ); modules = resolver.ModList().Memoize(); } @@ -1166,7 +1238,7 @@ public void Upgrade(IEnumerable modules, .Where(im => !removingIdents.Contains(im.identifier)) .Concat(modules.Select(m => new InstalledModule(null, m, Array.Empty(), false))) .ToList(), - ksp.VersionCriteria()) + instance.VersionCriteria()) .ToList(); if (autoRemoving.Count > 0) { @@ -1277,7 +1349,7 @@ public void Replace(IEnumerable replacements, RelationshipRes } } } - var resolver = new RelationshipResolver(modsToInstall, null, options, registry_manager.registry, ksp.VersionCriteria()); + var resolver = new RelationshipResolver(modsToInstall, null, options, registry_manager.registry, instance.VersionCriteria()); var resolvedModsToInstall = resolver.ModList().ToList(); AddRemove( ref possibleConfigOnlyDirs, @@ -1297,18 +1369,7 @@ public static IEnumerable PrioritizedHosts(IEnumerable urls) .Select(dl => dl.Host) .Distinct(); - /// - /// Makes sure all the specified mods are downloaded. - /// - private void DownloadModules(IEnumerable mods, IDownloader downloader) - { - List downloads = mods.Where(module => !Cache.IsCached(module)).ToList(); - - if (downloads.Count > 0) - { - downloader.DownloadModules(downloads); - } - } + #region Recommendations /// /// Looks for optional related modules that could be installed alongside the given modules @@ -1328,7 +1389,7 @@ public bool FindRecommendations(HashSet out Dictionary> suggestions, out Dictionary> supporters) { - var crit = ksp.VersionCriteria(); + var crit = instance.VersionCriteria(); var resolver = new RelationshipResolver(sourceModules.Where(m => !m.IsDLC), null, RelationshipResolverOptions.KitchenSinkOpts(), @@ -1438,6 +1499,8 @@ public bool CanInstall(List toInstall, return false; } + #endregion + /// /// Import a list of files into the download cache, with progress bar and /// interactive prompts for installation and deletion. @@ -1466,7 +1529,7 @@ public void ImportFiles(HashSet files, IUser user, Action List matches = index[sha1]; foreach (CkanModule mod in matches) { - if (mod.IsCompatible(ksp.VersionCriteria())) + if (mod.IsCompatible(instance.VersionCriteria())) { installable.Add(mod); } @@ -1490,7 +1553,7 @@ public void ImportFiles(HashSet files, IUser user, Action } if (installable.Count > 0 && user.RaiseYesNoDialog(string.Format( Properties.Resources.ModuleInstallerImportInstallPrompt, - installable.Count, ksp.Name, ksp.GameDir()))) + installable.Count, instance.Name, instance.GameDir()))) { // Install the imported mods foreach (CkanModule mod in installable) diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index f328a56f1f..4ceea0b846 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -829,10 +829,10 @@ public List LatestAvailableWithProvides( /// Register the supplied module as having been installed, thereby keeping /// track of its metadata and files. /// - public void RegisterModule(CkanModule mod, - IEnumerable absoluteFiles, - GameInstance inst, - bool autoInstalled) + public void RegisterModule(CkanModule mod, + List absoluteFiles, + GameInstance inst, + bool autoInstalled) { log.DebugFormat("Registering module {0}", mod); EnlistWithTransaction(); diff --git a/Core/Repositories/ProgressFilesOffsetsToPercent.cs b/Core/Repositories/ProgressFilesOffsetsToPercent.cs index bf37e3905a..c732a11372 100644 --- a/Core/Repositories/ProgressFilesOffsetsToPercent.cs +++ b/Core/Repositories/ProgressFilesOffsetsToPercent.cs @@ -29,12 +29,15 @@ public ProgressFilesOffsetsToPercent(IProgress percentProgress, /// How far into the current file we are public void Report(long currentFileOffset) { - var percent = basePercent + (int)(100 * currentFileOffset / totalSize); - // Only report each percentage once, to avoid spamming UI calls - if (percent > lastPercent) + if (totalSize > 0) { - percentProgress.Report(percent); - lastPercent = percent; + var percent = basePercent + (int)(100 * currentFileOffset / totalSize); + // Only report each percentage once, to avoid spamming UI calls + if (percent > lastPercent) + { + percentProgress.Report(percent); + lastPercent = percent; + } } } @@ -44,7 +47,10 @@ public void Report(long currentFileOffset) public void NextFile() { doneSize += sizes[currentIndex]; - basePercent = (int)(100 * doneSize / totalSize); + if (totalSize > 0) + { + basePercent = (int)(100 * doneSize / totalSize); + } ++currentIndex; if (basePercent > lastPercent) { diff --git a/Core/Repositories/ProgressScalePercentsByFileSize.cs b/Core/Repositories/ProgressScalePercentsByFileSize.cs index 0dc9a2046c..e726190482 100644 --- a/Core/Repositories/ProgressScalePercentsByFileSize.cs +++ b/Core/Repositories/ProgressScalePercentsByFileSize.cs @@ -29,7 +29,7 @@ public ProgressScalePercentsByFileSizes(IProgress percentProgress, /// How far into the current file we are public void Report(int currentFilePercent) { - if (basePercent < 100 && currentIndex < sizes.Length) + if (basePercent < 100 && currentIndex < sizes.Length && totalSize > 0) { var percent = basePercent + (int)(currentFilePercent * sizes[currentIndex] / totalSize); // Only report each percentage once, to avoid spamming UI calls @@ -47,7 +47,10 @@ public void Report(int currentFilePercent) public void NextFile() { doneSize += sizes[currentIndex]; - basePercent = (int)(100 * doneSize / totalSize); + if (totalSize > 0) + { + basePercent = (int)(100 * doneSize / totalSize); + } ++currentIndex; if (basePercent > lastPercent) { diff --git a/Tests/CmdLine/UpgradeTests.cs b/Tests/CmdLine/UpgradeTests.cs index 7e5b8b2926..6c5ac39c5b 100644 --- a/Tests/CmdLine/UpgradeTests.cs +++ b/Tests/CmdLine/UpgradeTests.cs @@ -79,7 +79,7 @@ public void RunCommand_IdentifierEqualsVersionSyntax_UpgradesToCorrectVersion( regMgr.registry.RepositoriesAdd(repo.repo); var fromModule = regMgr.registry.GetModuleByVersion(identifier, fromVersion); var toModule = regMgr.registry.GetModuleByVersion(identifier, toVersion); - regMgr.registry.RegisterModule(fromModule, Enumerable.Empty(), inst.KSP, false); + regMgr.registry.RegisterModule(fromModule, new List(), inst.KSP, false); manager.Cache.Store(toModule, TestData.DogeCoinFlagZip(), null); var opts = new UpgradeOptions() { @@ -598,7 +598,7 @@ public void RunCommand_VersionDependsUpgrade_UpgradesCorrectly(string descript foreach (var fromModule in instMods) { regMgr.registry.RegisterModule(fromModule, - Enumerable.Empty(), + new List(), inst.KSP, false); } // Pre-store mods that might be installed diff --git a/Tests/Core/ModuleInstallerTests.cs b/Tests/Core/ModuleInstallerTests.cs index 62baffd60b..ec030a8ce2 100644 --- a/Tests/Core/ModuleInstallerTests.cs +++ b/Tests/Core/ModuleInstallerTests.cs @@ -319,7 +319,7 @@ public void DontOverWrite_208() Assert.Throws(delegate { - ModuleInstaller.CopyZipEntry(zipfile, entry, tmpfile, false); + ModuleInstaller.CopyZipEntry(zipfile, entry, tmpfile, false, null); }); // Cleanup @@ -357,7 +357,7 @@ private string CopyDogeFromZip() // We have to delete our temporary file, as CZE refuses to overwrite; huzzah! File.Delete(tmpfile); - ModuleInstaller.CopyZipEntry(zipfile, entry, tmpfile, false); + ModuleInstaller.CopyZipEntry(zipfile, entry, tmpfile, false, null); return tmpfile; } @@ -662,7 +662,7 @@ public void GroupFilesByRemovable_WithFiles_CorrectOutput(string relRoot, var registry = RegistryManager.Instance(inst.KSP, repoData.Manager).registry; // Make files to be registered to another mod var absFiles = registeredFiles.Select(f => inst.KSP.ToAbsoluteGameDir(f)) - .ToArray(); + .ToList(); foreach (var absPath in absFiles) { Directory.CreateDirectory(Path.GetDirectoryName(absPath)); @@ -874,7 +874,7 @@ public void Replace_WithCompatibleModule_Succeeds() var downloader = new NetAsyncModulesDownloader(nullUser, manager.Cache); // Act - registry.RegisterModule(replaced, Enumerable.Empty(), inst.KSP, false); + registry.RegisterModule(replaced, new List(), inst.KSP, false); manager.Cache.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); var replacement = querier.GetReplacement(replaced.identifier, new GameVersionCriteria(new GameVersion(1, 12))); @@ -940,7 +940,7 @@ public void Replace_WithIncompatibleModule_Fails() var downloader = new NetAsyncModulesDownloader(nullUser, manager.Cache); // Act - registry.RegisterModule(replaced, Enumerable.Empty(), inst.KSP, false); + registry.RegisterModule(replaced, new List(), inst.KSP, false); manager.Cache.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); var replacement = querier.GetReplacement(replaced.identifier, new GameVersionCriteria(new GameVersion(1, 11))); @@ -1006,14 +1006,14 @@ public void UninstallList_WithAutoInst_RemovesAutoRemovable(string[] regularMods foreach (var m in regularMods) { registry.RegisterModule(CkanModule.FromJson(m), - Enumerable.Empty(), + new List(), inst.KSP, false); } foreach (var m in autoInstMods) { registry.RegisterModule(CkanModule.FromJson(m), - Enumerable.Empty(), + new List(), inst.KSP, true); } @@ -1098,7 +1098,7 @@ public void Upgrade_WithAutoInst_RemovesAutoRemovable(string[] regularMods, if (!querier.IsInstalled(module.identifier, false)) { registry.RegisterModule(module, - Enumerable.Empty(), + new List(), inst.KSP, false); } @@ -1106,7 +1106,7 @@ public void Upgrade_WithAutoInst_RemovesAutoRemovable(string[] regularMods, foreach (var m in autoInstMods) { registry.RegisterModule(CkanModule.FromJson(m), - Enumerable.Empty(), + new List(), inst.KSP, true); } diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index 56a68c1170..3c7e9043af 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Transactions; using System.Collections.Generic; @@ -316,8 +315,8 @@ public void HasUpdate_OtherModDependsOnCurrent_ReturnsFalse() CkanModule dependingMod = registry.GetModuleByVersion("DependingMod", "1.0"); GameInstance gameInst = gameInstWrapper.KSP; - registry.RegisterModule(olderDepMod, Array.Empty(), gameInst, false); - registry.RegisterModule(dependingMod, Array.Empty(), gameInst, false); + registry.RegisterModule(olderDepMod, new List(), gameInst, false); + registry.RegisterModule(dependingMod, new List(), gameInst, false); GameVersionCriteria crit = new GameVersionCriteria(olderDepMod.ksp_version); // Act @@ -420,7 +419,7 @@ public void TxEmbeddedCommit() using (var tScope = new TransactionScope()) { reg = CKAN.Registry.Empty(); - reg.RegisterModule(module, Enumerable.Empty(), + reg.RegisterModule(module, new List(), gameInstWrapper.KSP, false); CollectionAssert.AreEqual( @@ -451,7 +450,7 @@ public void TxCommit() using (var gameInstWrapper = new DisposableKSP()) using (var tScope = new TransactionScope()) { - registry.RegisterModule(module, Enumerable.Empty(), + registry.RegisterModule(module, new List(), gameInstWrapper.KSP, false); CollectionAssert.AreEqual( @@ -481,7 +480,7 @@ public void TxRollback() ""version"": ""1.0"", ""download"": ""https://github.com/"" }"); - registry.RegisterModule(module, Enumerable.Empty(), + registry.RegisterModule(module, new List(), gameInstWrapper.KSP, false); CollectionAssert.AreEqual( @@ -515,7 +514,7 @@ public void TxNested() ""version"": ""1.0"", ""download"": ""https://github.com/"" }"); - registry.RegisterModule(module, Enumerable.Empty(), + registry.RegisterModule(module, new List(), gameInstWrapper.KSP, false); using (var tScope2 = new TransactionScope(TransactionScopeOption.RequiresNew)) @@ -528,7 +527,7 @@ public void TxNested() ""version"": ""1.0"", ""download"": ""https://github.com/"" }"); - registry.RegisterModule(module2, Enumerable.Empty(), + registry.RegisterModule(module2, new List(), gameInstWrapper.KSP, false); }); tScope2.Complete(); @@ -553,7 +552,7 @@ public void TxAmbient() ""version"": ""1.0"", ""download"": ""https://github.com/"" }"); - registry.RegisterModule(module, Enumerable.Empty(), + registry.RegisterModule(module, new List(), gameInstWrapper.KSP, false); using (var tScope2 = new TransactionScope()) @@ -566,7 +565,7 @@ public void TxAmbient() ""version"": ""1.0"", ""download"": ""https://github.com/"" }"); - registry.RegisterModule(module2, Enumerable.Empty(), + registry.RegisterModule(module2, new List(), gameInstWrapper.KSP, false); }); tScope2.Complete(); diff --git a/Tests/Core/Registry/RegistryManager.cs b/Tests/Core/Registry/RegistryManager.cs index 9794496ea8..3934a00c8d 100644 --- a/Tests/Core/Registry/RegistryManager.cs +++ b/Tests/Core/Registry/RegistryManager.cs @@ -199,7 +199,7 @@ public void ScanUnmanagedFiles_WithDLLs_FindsUnregisteredOnly(string[] registere var regMgr = RegistryManager.Instance(gameInst, repoData.Manager); var registry = regMgr.registry; var absReg = registered.Select(p => gameInst.ToAbsoluteGameDir(p)) - .ToArray(); + .ToList(); var absUnreg = unregistered.Select(p => gameInst.ToAbsoluteGameDir(p)) .ToArray(); diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolver.cs index fedab95380..4c3df45245 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolver.cs @@ -312,7 +312,7 @@ public void ModList_WithInstalledModules_ContainsThemWithReasonInstalled() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { mod_a }; - registry.RegisterModule(mod_a, Array.Empty(), null, false); + registry.RegisterModule(mod_a, new List(), null, false); var relationship_resolver = new RelationshipResolver( list, null, options, registry, null); @@ -1036,8 +1036,8 @@ public void UninstallingConflictingModule_InstallingRecursiveDependencies_Resolv var registry = new CKAN.Registry(repoData.Manager, repo.repo); // Start with eve and eveDefaultConfig installed - registry.RegisterModule(eve, Array.Empty(), ksp.KSP, false); - registry.RegisterModule(eveDefaultConfig, Array.Empty(), ksp.KSP, false); + registry.RegisterModule(eve, new List(), ksp.KSP, false); + registry.RegisterModule(eveDefaultConfig, new List(), ksp.KSP, false); Assert.DoesNotThrow(() => registry.CheckSanity()); diff --git a/Tests/GUI/Model/GUIMod.cs b/Tests/GUI/Model/GUIMod.cs index 09ffa7ffa3..448c577479 100644 --- a/Tests/GUI/Model/GUIMod.cs +++ b/Tests/GUI/Model/GUIMod.cs @@ -63,7 +63,7 @@ public void HasUpdate_UpdateAvailable_ReturnsTrue() { var registry = new Registry(repoData.Manager, repo.repo); - registry.RegisterModule(old_version, Enumerable.Empty(), null, false); + registry.RegisterModule(old_version, new List(), null, false); var upgradeableGroups = registry.CheckUpgradeable(tidy.KSP.VersionCriteria(), new HashSet()); diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index c97e0220e1..2d727b876d 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -161,7 +161,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() // Install module and set it as pre-installed manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), new Progress(bytes => {})); - registry.RegisterModule(anyVersionModule, Array.Empty(), instance.KSP, false); + registry.RegisterModule(anyVersionModule, new List(), instance.KSP, false); HashSet possibleConfigOnlyDirs = null; installer.InstallList(