Exemple #1
0
        /// <summary>
        /// Stores all of our files in the cache once done.
        /// Called by NetAsyncDownloader on completion.
        /// Called with all nulls on download cancellation.
        /// </summary>
        private void ModuleDownloadsComplete(NetModuleCache cache, Uri[] urls, string[] filenames, Exception[] errors)
        {
            if (filenames != null)
            {
                for (int i = 0; i < errors.Length; i++)
                {
                    if (errors[i] == null)
                    {
                        // Cache the downloads that succeeded.
                        try
                        {
                            cache.Store(modules[i], filenames[i], modules[i].StandardName());
                        }
                        catch (FileNotFoundException e)
                        {
                            log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message);
                        }
                    }
                }

                // Finally, remove all our temp files.
                // We probably *could* have used Store's integrated move function above, but if we managed
                // to somehow get two URLs the same in our download set, that could cause right troubles!
                foreach (string tmpfile in filenames)
                {
                    log.DebugFormat("Cleaning up {0}", tmpfile);
                    File.Delete(tmpfile);
                }
            }
        }
Exemple #2
0
        private IEnumerable <ListViewItem> getRecSugRows(
            NetModuleCache cache,
            Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations,
            Dictionary <CkanModule, List <string> > suggestions,
            Dictionary <CkanModule, HashSet <string> > supporters
            )
        {
            foreach (var kvp in recommendations)
            {
                yield return(getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value.Item2),
                                           RecommendationsGroup, kvp.Value.Item1));
            }

            foreach (var kvp in suggestions)
            {
                yield return(getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value),
                                           SuggestionsGroup, false));
            }

            foreach (var kvp in supporters
                     .ToDictionary(kvp => kvp.Key, kvp => string.Join(", ", kvp.Value.OrderBy(s => s)))
                     .OrderBy(kvp => kvp.Value)
                     )
            {
                yield return(getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value),
                                           SupportedByGroup, false));
            }
        }
Exemple #3
0
        /// <summary>
        /// Import a list of files into the download cache, with progress bar and
        /// interactive prompts for installation and deletion.
        /// </summary>
        /// <param name="files">Set of files to import</param>
        /// <param name="user">Object for user interaction</param>
        /// <param name="installMod">Function to call to mark a mod for installation</param>
        /// <param name="allowDelete">True to ask user whether to delete imported files, false to leave the files as is</param>
        public void ImportFiles(HashSet <FileInfo> files, IUser user, Action <string> installMod, bool allowDelete = true)
        {
            Registry         registry    = registry_manager.registry;
            HashSet <string> installable = new HashSet <string>();
            List <FileInfo>  deletable   = new List <FileInfo>();
            // Get the mapping of known hashes to modules
            Dictionary <string, List <CkanModule> > index = registry.GetSha1Index();
            int i = 0;

            foreach (FileInfo f in files)
            {
                int percent = i * 100 / files.Count;
                user.RaiseProgress($"Importing {f.Name}... ({percent}%)", percent);
                // Calc SHA-1 sum
                string sha1 = NetModuleCache.GetFileHashSha1(f.FullName);
                // Find SHA-1 sum in registry (potentially multiple)
                if (index.ContainsKey(sha1))
                {
                    deletable.Add(f);
                    List <CkanModule> matches = index[sha1];
                    foreach (CkanModule mod in matches)
                    {
                        if (mod.IsCompatibleKSP(ksp.VersionCriteria()))
                        {
                            installable.Add(mod.identifier);
                        }
                        if (Cache.IsMaybeCachedZip(mod))
                        {
                            user.RaiseMessage("Already cached: {0}", f.Name);
                        }
                        else
                        {
                            user.RaiseMessage($"Importing {mod.identifier} {StripEpoch(mod.version)}...");
                            Cache.Store(mod, f.FullName);
                        }
                    }
                }
                else
                {
                    user.RaiseMessage("Not found in index: {0}", f.Name);
                }
                ++i;
            }
            if (installable.Count > 0 && user.RaiseYesNoDialog($"Install {installable.Count} compatible imported mods in game instance {ksp.Name} ({ksp.GameDir()})?"))
            {
                // Install the imported mods
                foreach (string identifier in installable)
                {
                    installMod(identifier);
                }
            }
            if (allowDelete && deletable.Count > 0 && user.RaiseYesNoDialog($"Import complete. Delete {deletable.Count} old files?"))
            {
                // Delete old files
                foreach (FileInfo f in deletable)
                {
                    f.Delete();
                }
            }
        }
Exemple #4
0
        public void LoadProviders(string requested, List <CkanModule> modules, NetModuleCache cache)
        {
            Util.Invoke(this, () =>
            {
                ChooseProvidedModsLabel.Text = String.Format(
                    Properties.Resources.MainInstallProvidedBy,
                    requested
                    );

                ChooseProvidedModsListView.Items.Clear();
                ChooseProvidedModsListView.Items.AddRange(modules
                                                          .Select(module => new ListViewItem(new string[]
                {
                    cache.IsMaybeCachedZip(module)
                            ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version)
                            : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)),
                    module.@abstract
                })
                {
                    Tag     = module,
                    Checked = false
                })
                                                          .ToArray());
                ChooseProvidedModsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
                ChooseProvidedModsContinueButton.Enabled = false;
            });
        }
Exemple #5
0
        /// <summary>
        /// Downloads the given mod to the cache. Returns the filename it was saved to.
        /// </summary>
        public static string Download(CkanModule module, string filename, NetModuleCache cache)
        {
            log.Info("Downloading " + filename);

            string tmp_file = Net.Download(module.download);

            return(cache.Store(module, tmp_file, filename, true));
        }
 /// <summary>
 /// Returns a perfectly boring NetAsyncModulesDownloader.
 /// </summary>
 public NetAsyncModulesDownloader(IUser user, NetModuleCache cache)
 {
     modules    = new List <CkanModule>();
     downloader = new NetAsyncDownloader(user)
     {
         // Schedule us to process each module on completion.
         onOneCompleted = ModuleDownloadComplete
     };
     this.cache = cache;
 }
        /// <summary>
        /// Stores all of our files in the cache once done.
        /// Called by NetAsyncDownloader on completion.
        /// Called with all nulls on download cancellation.
        /// </summary>
        private void ModuleDownloadsComplete(NetModuleCache cache, Uri[] urls, string[] filenames, Exception[] errors)
        {
            if (urls != null)
            {
                // spawn up to 3 dialogs
                int errorDialogsLeft = 3;

                for (int i = 0; i < errors.Length; i++)
                {
                    if (errors[i] != null)
                    {
                        if (errorDialogsLeft > 0)
                        {
                            User.RaiseError("Failed to download \"{0}\" - error: {1}", urls[i], errors[i].Message);
                            errorDialogsLeft--;
                        }
                    }
                    else
                    {
                        // Even if some of our downloads failed, we want to cache the
                        // ones which succeeded.

                        // This doesn't work :(
                        // for some reason the tmp files get deleted before we get here and we get a nasty exception
                        // not only that but then we try _to install_ the rest of the mods and then CKAN crashes
                        // and the user's registry gets corrupted forever
                        // commenting out until this is resolved
                        // ~ nlight

                        try
                        {
                            cache.Store(modules[i], filenames[i], modules[i].StandardName());
                        }
                        catch (FileNotFoundException e)
                        {
                            log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message);
                        }
                    }
                }
            }

            if (filenames != null)
            {
                // Finally, remove all our temp files.
                // We probably *could* have used Store's integrated move function above, but if we managed
                // to somehow get two URLs the same in our download set, that could cause right troubles!

                foreach (string tmpfile in filenames)
                {
                    log.DebugFormat("Cleaning up {0}", tmpfile);
                    File.Delete(tmpfile);
                }
            }
        }
Exemple #8
0
 public void LoadRecommendations(
     NetModuleCache cache,
     Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations,
     Dictionary <CkanModule, List <string> > suggestions,
     Dictionary <CkanModule, HashSet <string> > supporters
     )
 {
     RecommendedModsToggleCheckbox.Checked = true;
     RecommendedModsListView.Items.Clear();
     RecommendedModsListView.Items.AddRange(
         getRecSugRows(cache, recommendations, suggestions, supporters).ToArray());
 }
Exemple #9
0
 /// <summary>
 /// Returns a KSP object.
 /// Will initialise a CKAN instance in the KSP dir if it does not already exist,
 /// if the directory contains a valid KSP install.
 /// </summary>
 public KSP(string gameDir, string name, IUser user)
 {
     Name = name;
     User = user;
     // Make sure our path is absolute and has normalised slashes.
     this.gameDir = KSPPathUtils.NormalizePath(Path.GetFullPath(gameDir));
     if (Valid)
     {
         SetupCkanDirectories();
         LoadCompatibleVersions();
         Cache = new NetModuleCache(DownloadCacheDir());
     }
 }
Exemple #10
0
 private ListViewItem getRecSugItem(NetModuleCache cache, CkanModule module, string descrip, ListViewGroup group, bool check)
 {
     return(new ListViewItem(new string[]
     {
         cache.IsMaybeCachedZip(module)
             ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version)
             : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)),
         descrip,
         module.@abstract
     })
     {
         Tag = module,
         Checked = check,
         Group = group
     });
 }
Exemple #11
0
        /// <summary>
        /// Returns the path to a cached copy of a module if it exists, or downloads
        /// and returns the downloaded copy otherwise.
        ///
        /// If no filename is provided, the module's standard name will be used.
        /// Chcecks provided cache first.
        /// </summary>
        public static string CachedOrDownload(CkanModule module, NetModuleCache cache, string filename = null)
        {
            if (filename == null)
            {
                filename = CkanModule.StandardName(module.identifier, module.version);
            }

            string full_path = cache.GetCachedZip(module);

            if (full_path == null)
            {
                return(Download(module, filename, cache));
            }

            log.DebugFormat("Using {0} (cached)", filename);
            return(full_path);
        }
Exemple #12
0
 public void LoadRecommendations(
     IRegistryQuerier registry, KspVersionCriteria kspVersion, NetModuleCache cache,
     Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations,
     Dictionary <CkanModule, List <string> > suggestions,
     Dictionary <CkanModule, HashSet <string> > supporters
     )
 {
     this.registry   = registry;
     this.kspVersion = kspVersion;
     Util.Invoke(this, () =>
     {
         RecommendedModsToggleCheckbox.Checked = true;
         RecommendedModsListView.Items.Clear();
         RecommendedModsListView.Items.AddRange(
             getRecSugRows(cache, recommendations, suggestions, supporters).ToArray());
     });
 }
 /// <summary>
 /// Returns a perfectly boring NetAsyncModulesDownloader.
 /// </summary>
 public NetAsyncModulesDownloader(IUser user, NetModuleCache cache)
 {
     modules    = new List <CkanModule>();
     downloader = new NetAsyncDownloader(user)
     {
         // Schedule us to process each module on completion.
         onOneCompleted = ModuleDownloadComplete
     };
     downloader.Progress += (target, remaining, total) =>
     {
         var mod = modules.FirstOrDefault(m => m.download == target.url);
         if (mod != null && Progress != null)
         {
             Progress(mod, remaining, total);
         }
     };
     this.cache = cache;
 }
 public void LoadRecommendations(
     IRegistryQuerier registry,
     HashSet <CkanModule> toInstall, HashSet <CkanModule> toUninstall,
     GameVersionCriteria GameVersion, NetModuleCache cache,
     Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations,
     Dictionary <CkanModule, List <string> > suggestions,
     Dictionary <CkanModule, HashSet <string> > supporters
     )
 {
     this.registry    = registry;
     this.toInstall   = toInstall;
     this.toUninstall = toUninstall;
     this.GameVersion = GameVersion;
     Util.Invoke(this, () =>
     {
         RecommendedModsToggleCheckbox.Checked = true;
         RecommendedModsListView.Items.AddRange(
             getRecSugRows(cache, recommendations, suggestions, supporters).ToArray());
         MarkConflicts();
     });
 }
Exemple #15
0
        /// <summary>
        /// <see cref="IDownloader.DownloadModules(NetFileCache, IEnumerable{CkanModule})"/>
        /// </summary>
        public void DownloadModules(NetModuleCache cache, IEnumerable <CkanModule> modules)
        {
            // Walk through all our modules, but only keep the first of each
            // one that has a unique download path.
            Dictionary <Uri, CkanModule> unique_downloads = modules
                                                            .GroupBy(module => module.download)
                                                            .ToDictionary(p => p.First().download, p => p.First());

            this.modules.AddRange(unique_downloads.Values);

            // Schedule us to process our modules on completion.
            downloader.onCompleted =
                (_uris, paths, errors) =>
                ModuleDownloadsComplete(cache, _uris, paths, errors);

            try
            {
                // Start the downloads!
                downloader.DownloadAndWait(
                    unique_downloads.Select(item => new Net.DownloadTarget(
                                                item.Key,
                                                item.Value.InternetArchiveDownload,
                                                // Use a temp file name
                                                null,
                                                item.Value.download_size,
                                                // Send the MIME type to use for the Accept header
                                                // The GitHub API requires this to include application/octet-stream
                                                string.IsNullOrEmpty(item.Value.download_content_type)
                            ? defaultMimeType
                            : $"{item.Value.download_content_type};q=1.0,{defaultMimeType};q=0.9"
                                                )).ToList()
                    );
            }
            catch (DownloadErrorsKraken kraken)
            {
                // Associate the errors with the affected modules
                throw new ModuleDownloadErrorsKraken(this.modules, kraken);
            }
        }
        /// <summary>
        /// <see cref="IDownloader.DownloadModules(NetFileCache, IEnumerable{CkanModule})"/>
        /// </summary>
        public void DownloadModules(NetModuleCache cache, IEnumerable <CkanModule> modules)
        {
            // Walk through all our modules, but only keep the first of each
            // one that has a unique download path.
            Dictionary <Uri, CkanModule> unique_downloads = modules
                                                            .GroupBy(module => module.download)
                                                            .ToDictionary(p => p.First().download, p => p.First());

            this.modules.AddRange(unique_downloads.Values);

            // Schedule us to process our modules on completion.
            downloader.onCompleted =
                (_uris, paths, errors) =>
                ModuleDownloadsComplete(cache, _uris, paths, errors);

            // retrieve the expected download size for each mod
            List <KeyValuePair <Uri, long> > downloads_with_size = unique_downloads
                                                                   .Select(item => new KeyValuePair <Uri, long>(item.Key, item.Value.download_size))
                                                                   .ToList();

            // Start the download!
            downloader.DownloadAndWait(downloads_with_size);
        }
        /// <summary>
        /// Switch to using a download cache in a new location
        /// </summary>
        /// <param name="path">Location of folder for new cache</param>
        /// <param name="failureReason">Contains a human readable failure message if the setup failed</param>
        /// <returns>
        /// true if successful, false otherwise
        /// </returns>
        public bool TrySetupCache(string path, out string failureReason)
        {
            string origPath = Configuration.DownloadCacheDir;

            try
            {
                if (string.IsNullOrEmpty(path))
                {
                    Configuration.DownloadCacheDir = "";
                    Cache = new NetModuleCache(this, Configuration.DownloadCacheDir);
                }
                else
                {
                    Cache = new NetModuleCache(this, path);
                    Configuration.DownloadCacheDir = path;
                }
                failureReason = null;
                return(true);
            }
            catch (DirectoryNotFoundKraken)
            {
                failureReason = $"{path} does not exist";
                return(false);
            }
            catch (PathErrorKraken ex)
            {
                failureReason = ex.Message;
                return(false);
            }
            catch (IOException ex)
            {
                // MoveFrom failed, possibly full disk, so undo the change
                Configuration.DownloadCacheDir = origPath;
                failureReason = ex.Message;
                return(false);
            }
        }
Exemple #18
0
        /// <summary>
        /// Download and update the local CKAN meta-info.
        /// Optionally takes a URL to the zipfile repo to download.
        /// Returns the number of unique modules updated.
        /// </summary>
        public static int UpdateAllRepositories(RegistryManager registry_manager, KSP ksp, NetModuleCache cache, IUser user)
        {
            SortedDictionary <string, Repository> sortedRepositories = registry_manager.registry.Repositories;
            List <CkanModule> allAvail = new List <CkanModule>();

            foreach (KeyValuePair <string, Repository> repository in sortedRepositories)
            {
                log.InfoFormat("About to update {0}", repository.Value.name);
                SortedDictionary <string, int> downloadCounts;
                List <CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts);
                registry_manager.registry.SetDownloadCounts(downloadCounts);
                if (avail == null)
                {
                    // Report failure if any repo fails, rather than losing half the list.
                    // UpdateRegistry will have alerted the user to specific errors already.
                    return(0);
                }
                else
                {
                    log.InfoFormat("Updated {0}", repository.Value.name);
                    // Merge all the lists
                    allAvail.AddRange(avail);
                }
            }
            // Save allAvail to the registry if we found anything
            if (allAvail.Count > 0)
            {
                registry_manager.registry.SetAllAvailable(allAvail);
                // Save our changes.
                registry_manager.Save(enforce_consistency: false);

                ShowUserInconsistencies(registry_manager.registry, user);

                List <CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry);
                if (metadataChanges.Count > 0)
                {
                    HandleModuleChanges(metadataChanges, user, ksp, cache);
                }

                // Return how many we got!
                return(registry_manager.registry.Available(ksp.VersionCriteria()).Count);
            }
            else
            {
                // Return failure
                return(0);
            }
        }
Exemple #19
0
        /// <summary>
        /// Resolve differences between installed and available metadata for given ModuleInstaller
        /// </summary>
        /// <param name="metadataChanges">List of modules that changed</param>
        /// <param name="user">Object for user interaction callbacks</param>
        /// <param name="ksp">Game instance</param>
        private static void HandleModuleChanges(List <CkanModule> metadataChanges, IUser user, KSP ksp, NetModuleCache cache)
        {
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < metadataChanges.Count; i++)
            {
                CkanModule module = metadataChanges[i];
                sb.AppendLine(string.Format("- {0} {1}", module.identifier, module.version));
            }

            if (user.RaiseYesNoDialog(string.Format(@"The following mods have had their metadata changed since last update:

{0}
You should reinstall them in order to preserve consistency with the repository.

Do you wish to reinstall now?", sb)))
            {
                ModuleInstaller installer = ModuleInstaller.GetInstance(ksp, cache, new NullUser());
                // New upstream metadata may break the consistency of already installed modules
                // e.g. if user installs modules A and B and then later up A is made to conflict with B
                // This is perfectly normal and shouldn't produce an error, therefore we skip enforcing
                // consistency. However, we will show the user any inconsistencies later on.

                // Use the identifiers so we use the overload that actually resolves relationships
                // Do each changed module one at a time so a failure of one doesn't cause all the others to fail
                foreach (string changedIdentifier in metadataChanges.Select(i => i.identifier))
                {
                    try
                    {
                        installer.Upgrade(
                            new[] { changedIdentifier },
                            new NetAsyncModulesDownloader(new NullUser()),
                            enforceConsistency: false
                            );
                    }
                    // Thrown when a dependency couldn't be satisfied
                    catch (ModuleNotFoundKraken)
                    {
                        log.WarnFormat("Skipping installation of {0} due to relationship error.", changedIdentifier);
                        user.RaiseMessage("Skipping installation of {0} due to relationship error.", changedIdentifier);
                    }
                    // Thrown when a conflicts relationship is violated
                    catch (InconsistentKraken)
                    {
                        log.WarnFormat("Skipping installation of {0} due to relationship error.", changedIdentifier);
                        user.RaiseMessage("Skipping installation of {0} due to relationship error.", changedIdentifier);
                    }
                }
            }
        }
Exemple #20
0
        /// <summary>
        /// Download and update the local CKAN meta-info.
        /// Optionally takes a URL to the zipfile repo to download.
        /// </summary>
        public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_manager, KSP ksp, NetModuleCache cache, IUser user)
        {
            SortedDictionary <string, Repository> sortedRepositories = registry_manager.registry.Repositories;

            if (sortedRepositories.Values.All(repo => !string.IsNullOrEmpty(repo.last_server_etag) && repo.last_server_etag == Net.CurrentETag(repo.uri)))
            {
                user?.RaiseMessage("No changes since last update");
                return(RepoUpdateResult.NoChanges);
            }
            List <CkanModule> allAvail = new List <CkanModule>();

            foreach (KeyValuePair <string, Repository> repository in sortedRepositories)
            {
                log.InfoFormat("About to update {0}", repository.Value.name);
                SortedDictionary <string, int> downloadCounts;
                string            newETag;
                List <CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts, out newETag);
                registry_manager.registry.SetDownloadCounts(downloadCounts);
                if (avail == null)
                {
                    // Report failure if any repo fails, rather than losing half the list.
                    // UpdateRegistry will have alerted the user to specific errors already.
                    return(RepoUpdateResult.Failed);
                }
                else
                {
                    log.InfoFormat("Updated {0}", repository.Value.name);
                    // Merge all the lists
                    allAvail.AddRange(avail);
                    repository.Value.last_server_etag = newETag;
                }
            }
            // Save allAvail to the registry if we found anything
            if (allAvail.Count > 0)
            {
                registry_manager.registry.SetAllAvailable(allAvail);
                // Save our changes.
                registry_manager.Save(enforce_consistency: false);

                ShowUserInconsistencies(registry_manager.registry, user);

                List <CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry);
                if (metadataChanges.Count > 0)
                {
                    HandleModuleChanges(metadataChanges, user, ksp, cache, registry_manager);
                }

                // Registry.CompatibleModules is slow, just return success,
                // caller can check it if it's really needed
                return(RepoUpdateResult.Updated);
            }
            else
            {
                // Return failure
                return(RepoUpdateResult.Failed);
            }
        }
Exemple #21
0
        /// <summary>
        /// Resolve differences between installed and available metadata for given ModuleInstaller
        /// </summary>
        /// <param name="metadataChanges">List of modules that changed</param>
        /// <param name="user">Object for user interaction callbacks</param>
        /// <param name="ksp">Game instance</param>
        /// <param name="cache">Cacne object for mod downloads</param>
        /// <param name="registry_manager">Manager that holds our game instances</param>
        private static void HandleModuleChanges(List <CkanModule> metadataChanges, IUser user, GameInstance ksp, NetModuleCache cache, RegistryManager registry_manager)
        {
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < metadataChanges.Count; i++)
            {
                CkanModule module = metadataChanges[i];
                sb.AppendLine(string.Format("- {0} {1}", module.identifier, module.version));
            }

            if (user.RaiseYesNoDialog(string.Format(@"The following mods have had their metadata changed since last update:

{0}
You should reinstall them in order to preserve consistency with the repository.

Do you wish to reinstall now?", sb)))
            {
                throw new ReinstallModuleKraken(metadataChanges);
            }
        }
Exemple #22
0
 /// <summary>
 /// Returns a perfectly boring NetAsyncModulesDownloader.
 /// </summary>
 public NetAsyncModulesDownloader(IUser user, NetModuleCache cache)
 {
     modules    = new List <CkanModule>();
     downloader = new NetAsyncDownloader(user);
     this.cache = cache;
 }
Exemple #23
0
        /// <summary>
        /// Download and update the local CKAN meta-info.
        /// Optionally takes a URL to the zipfile repo to download.
        /// </summary>
        public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_manager, GameInstance ksp, NetModuleCache cache, IUser user)
        {
            SortedDictionary <string, Repository> sortedRepositories = registry_manager.registry.Repositories;

            user.RaiseProgress("Checking for updates", 0);
            if (sortedRepositories.Values.All(repo => !string.IsNullOrEmpty(repo.last_server_etag) && repo.last_server_etag == Net.CurrentETag(repo.uri)))
            {
                user.RaiseProgress("Already up to date", 100);
                user.RaiseMessage("No changes since last update");
                return(RepoUpdateResult.NoChanges);
            }
            List <CkanModule> allAvail = new List <CkanModule>();
            int index = 0;

            foreach (KeyValuePair <string, Repository> repository in sortedRepositories)
            {
                user.RaiseProgress($"Updating {repository.Value.name}",
                                   10 + 80 * index / sortedRepositories.Count);
                SortedDictionary <string, int> downloadCounts;
                string            newETag;
                List <CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts, out newETag);
                registry_manager.registry.SetDownloadCounts(downloadCounts);
                if (avail == null)
                {
                    // Report failure if any repo fails, rather than losing half the list.
                    // UpdateRegistry will have alerted the user to specific errors already.
                    return(RepoUpdateResult.Failed);
                }
                else
                {
                    // Merge all the lists
                    allAvail.AddRange(avail);
                    repository.Value.last_server_etag = newETag;
                    user.RaiseMessage("Updated {0} ({1} modules)",
                                      repository.Value.name, avail.Count);
                }
                ++index;
            }
            // Save allAvail to the registry if we found anything
            if (allAvail.Count > 0)
            {
                user.RaiseProgress("Saving modules to registry", 90);
                using (var transaction = CkanTransaction.CreateTransactionScope())
                {
                    // Save our changes.
                    registry_manager.registry.SetAllAvailable(allAvail);
                    registry_manager.Save(enforce_consistency: false);
                    transaction.Complete();
                }

                ShowUserInconsistencies(registry_manager.registry, user);

                List <CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry);
                if (metadataChanges.Count > 0)
                {
                    HandleModuleChanges(metadataChanges, user, ksp, cache, registry_manager);
                }

                // Registry.CompatibleModules is slow, just return success,
                // caller can check it if it's really needed
                user.RaiseProgress("Registry saved", 100);
                user.RaiseMessage("Repositories updated");
                return(RepoUpdateResult.Updated);
            }
            else
            {
                // Return failure
                user.RaiseMessage("No modules found!");
                return(RepoUpdateResult.Failed);
            }
        }