/// <summary> /// Installs all modules given a list of identifiers as a transaction. Resolves dependencies. /// This *will* save the registry at the end of operation. /// /// Propagates a BadMetadataKraken if our install metadata is bad. /// Propagates a FileExistsKraken if we were going to overwrite a file. /// Propagates a CancelledActionKraken if the user cancelled the install. /// </summary> // // TODO: Break this up into smaller pieces! It's huge! public void InstallList( List <string> modules, RelationshipResolverOptions options, NetAsyncDownloader downloader = null ) { onReportProgress = onReportProgress ?? ((message, progress) => { }); var resolver = new RelationshipResolver(modules, options, registry_manager.registry); List <CkanModule> modsToInstall = resolver.ModList(); List <CkanModule> downloads = new List <CkanModule> (); // TODO: All this user-stuff should be happening in another method! // We should just be installing mods as a transaction. User.WriteLine("About to install...\n"); foreach (CkanModule module in modsToInstall) { if (!ksp.Cache.IsCachedZip(module.download)) { User.WriteLine(" * {0}", module); downloads.Add(module); } else { User.WriteLine(" * {0} (cached)", module); } } bool ok = User.YesNo("\nContinue?", FrontEndType.CommandLine); if (!ok) { throw new CancelledActionKraken("User declined install list"); } User.WriteLine(""); // Just to look tidy. if (downloads.Count > 0) { if (downloader == null) { downloader = new NetAsyncDownloader(); } downloader.DownloadModules(ksp.Cache, downloads, onReportProgress); } // We're about to install all our mods; so begin our transaction. var txoptions = new TransactionOptions(); txoptions.Timeout = TransactionManager.MaximumTimeout; using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required, txoptions)) { for (int i = 0; i < modsToInstall.Count; i++) { int percentComplete = (i * 100) / modsToInstall.Count; onReportProgress(String.Format("Installing mod \"{0}\"", modsToInstall[i]), percentComplete); Install(modsToInstall[i]); } onReportProgress("Updating registry", 70); registry_manager.Save(); onReportProgress("Commiting filesystem changes", 80); transaction.Complete(); } // We can scan GameData as a separate transaction. Installing the mods // leaves everything consistent, and this is just gravy. (And ScanGameData // acts as a Tx, anyway, so we don't need to provide our own.) onReportProgress("Rescanning GameData", 90); ksp.ScanGameData(); onReportProgress("Done!", 100); }
/// <summary> /// Downloads all the modules specified to the cache. /// Even if modules share download URLs, they will only be downloaded once. /// Blocks until the download is complete, cancelled, or errored. /// </summary> public void DownloadModules( NetFileCache cache, IEnumerable <CkanModule> modules, ModuleInstallerReportProgress progress = null ) { var unique_downloads = new Dictionary <Uri, CkanModule>(); // Walk through all our modules, but only keep the first of each // one that has a unique download path. foreach (CkanModule module in modules) { if (!unique_downloads.ContainsKey(module.download)) { unique_downloads[module.download] = module; } } // Attach our progress report, if requested. if (progress != null) { this.onProgressReport += (percent, bytesPerSecond, bytesLeft) => progress( String.Format("{0} kbps - downloading - {1} MiB left", bytesPerSecond / 1024, bytesLeft / 1024 / 1024), percent ); } this.onCompleted = (_uris, paths, errors) => ModuleDownloadsComplete(cache, _uris, paths, unique_downloads.Values.ToArray(), errors); // Start the download! this.Download(unique_downloads.Keys); // The Monitor.Wait function releases a lock, and then waits until it can re-acquire it. // Elsewhere, our downloading callback pulses the lock, which causes us to wake up and // continue. lock (download_complete_lock) { log.Debug("Waiting for downloads to finish..."); Monitor.Wait(download_complete_lock); } // If the user cancelled our progress, then signal that. if (downloadCanceled) { foreach (var download in downloads) { download.agent.CancelAsync(); } throw new CancelledActionKraken("Download cancelled by user"); } // Check to see if we've had any errors. If so, then release the kraken! List <Exception> exceptions = downloads .Select(x => x.error) .Where(ex => ex != null) .ToList(); // Let's check if any of these are certificate errors. If so, // we'll report that instead, as this is common (and user-fixable) // under Linux. foreach (Exception ex in exceptions) { if (ex is WebException && Regex.IsMatch(ex.Message, "authentication or decryption has failed")) { throw new MissingCertificateKraken(); } } if (exceptions.Count > 0) { throw new DownloadErrorsKraken(exceptions); } // Yay! Everything worked! }