diff --git a/Cmdline/Action/Install.cs b/Cmdline/Action/Install.cs index 81b76ef5ef..0cb7157c28 100644 --- a/Cmdline/Action/Install.cs +++ b/Cmdline/Action/Install.cs @@ -31,119 +31,88 @@ public Install(GameInstanceManager mgr, RepositoryDataManager repoData, IUser us /// public int RunCommand(CKAN.GameInstance instance, object raw_options) { - InstallOptions options = (InstallOptions) raw_options; + var options = raw_options as InstallOptions; + if (options.modules.Count == 0 && options.ckan_files == null) + { + // What? No mods specified? + user.RaiseMessage("{0}:", Properties.Resources.Usage); + user.RaiseMessage( + " ckan install Mod [Mod2, ...] [--with-suggests] [--with-all-suggests] [--no-recommends]"); + user.RaiseMessage( + " ckan install -c file_or_url.ckan [file_or_url2.ckan, ...] [--with-suggests] [--with-all-suggests] [--no-recommends]"); + return Exit.BADOPT; + } + + var regMgr = RegistryManager.Instance(instance, repoData); + List modules = null; if (options.ckan_files != null) { - // Oooh! We're installing from a CKAN file. - foreach (string ckan_file in options.ckan_files) + // Install from CKAN files + try { - Uri ckan_uri; - - // Check if the argument if a wellformatted Uri. - if (!Uri.IsWellFormedUriString(ckan_file, UriKind.Absolute)) - { - // Assume it is a local file, check if the file exists. - if (File.Exists(ckan_file)) - { - // Get the full path of the file. - ckan_uri = new Uri(Path.GetFullPath(ckan_file)); - } - else - { - // We have no further ideas as what we can do with this Uri, tell the user. - user.RaiseError(Properties.Resources.InstallNotFound, ckan_file); - return Exit.ERROR; - } - } - else - { - ckan_uri = new Uri(ckan_file); - } - - string filename = string.Empty; - - // If it is a local file, we already know the filename. If it is remote, create a temporary file and download the remote resource. - if (ckan_uri.IsFile) - { - filename = ckan_uri.LocalPath; - log.InfoFormat("Installing from local CKAN file \"{0}\"", filename); - } - else - { - log.InfoFormat("Installing from remote CKAN file \"{0}\"", ckan_uri); - filename = Net.Download(ckan_uri, null, user); - - log.DebugFormat("Temporary file for \"{0}\" is at \"{1}\".", ckan_uri, filename); - } - - // Parse the JSON file. - try - { - CkanModule m = MainClass.LoadCkanFromFile(filename); - options.modules.Add($"{m.identifier}={m.version}"); - } - catch (Kraken kraken) - { - user.RaiseError("{0}", - kraken.InnerException == null - ? kraken.Message - : $"{kraken.Message}: {kraken.InnerException.Message}"); - } + var targets = options.ckan_files + .Select(arg => new NetAsyncDownloader.DownloadTarget(getUri(arg))) + .ToList(); + log.DebugFormat("Urls: {0}", targets.SelectMany(t => t.urls)); + new NetAsyncDownloader(new NullUser()).DownloadAndWait(targets); + log.DebugFormat("Files: {0}", targets.Select(t => t.filename)); + modules = targets.Select(t => MainClass.LoadCkanFromFile(t.filename)) + .ToList(); + } + catch (FileNotFoundKraken kraken) + { + user.RaiseError(Properties.Resources.InstallNotFound, + kraken.file); + return Exit.ERROR; + } + catch (Kraken kraken) + { + user.RaiseError("{0}", + kraken.InnerException == null + ? kraken.Message + : $"{kraken.Message}: {kraken.InnerException.Message}"); + return Exit.ERROR; } - - // At times RunCommand() calls itself recursively - in this case we do - // not want to be doing this again, so "consume" the option - options.ckan_files = null; } else { - Search.AdjustModulesCase(instance, - RegistryManager.Instance(instance, repoData).registry, - options.modules); + var identifiers = options.modules; + var registry = regMgr.registry; + var installed = registry.InstalledModules + .Select(im => im.Module) + .ToArray(); + var crit = instance.VersionCriteria(); + Search.AdjustModulesCase(instance, registry, identifiers); + modules = identifiers.Select(arg => CkanModule.FromIDandVersion( + registry, arg, + options.allow_incompatible + ? null + : crit) + ?? registry.LatestAvailable(arg, crit, + null, installed) + ?? registry.InstalledModule(arg)?.Module) + .ToList(); } - if (options.modules.Count == 0) - { - // What? No files specified? - user.RaiseMessage( - $"{Properties.Resources.Usage}: ckan install [--with-suggests] [--with-all-suggests] [--no-recommends] [--headless] Mod [Mod2, ...]"); - return Exit.BADOPT; - } - - // Prepare options. Can these all be done in the new() somehow? + var installer = new ModuleInstaller(instance, manager.Cache, user); var install_ops = new RelationshipResolverOptions { - with_all_suggests = options.with_all_suggests, - with_suggests = options.with_suggests, - with_recommends = !options.no_recommends, - allow_incompatible = options.allow_incompatible + with_all_suggests = options.with_all_suggests, + with_suggests = options.with_suggests, + with_recommends = !options.no_recommends, + allow_incompatible = options.allow_incompatible, + without_toomanyprovides_kraken = user.Headless, + without_enforce_consistency = user.Headless, }; - if (user.Headless) - { - install_ops.without_toomanyprovides_kraken = true; - install_ops.without_enforce_consistency = true; - } - - var regMgr = RegistryManager.Instance(instance, repoData); - List modules = options.modules; - for (bool done = false; !done; ) { // Install everything requested. :) try { HashSet possibleConfigOnlyDirs = null; - var installer = new ModuleInstaller(instance, manager.Cache, user); - installer.InstallList(modules.Select(arg => CkanModule.FromIDandVersion( - regMgr.registry, arg, - options.allow_incompatible - ? null - : instance.VersionCriteria())) - .ToList(), - install_ops, - regMgr, + installer.InstallList(modules, install_ops, regMgr, ref possibleConfigOnlyDirs); user.RaiseMessage(""); done = true; @@ -199,7 +168,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } // Add the module to the list. - modules.Add($"{ex.modules[result].identifier}={ex.modules[result].version}"); + modules.Add(ex.modules[result]); // DON'T return so we can loop around and try again } catch (FileExistsKraken ex) @@ -275,6 +244,13 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) return Exit.OK; } + private Uri getUri(string arg) + => Uri.IsWellFormedUriString(arg, UriKind.Absolute) + ? new Uri(arg) + : File.Exists(arg) + ? new Uri(Path.GetFullPath(arg)) + : throw new FileNotFoundKraken(arg); + private readonly GameInstanceManager manager; private readonly RepositoryDataManager repoData; private readonly IUser user; diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 7d144b475d..060c216b15 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -230,11 +230,29 @@ private static bool MetadataChanged(this IRegistryQuerier querier, string identi /// /// String describing range of compatible game versions. /// - public static string CompatibleGameVersions(this IRegistryQuerier querier, IGame game, string identifier) + public static string CompatibleGameVersions(this IRegistryQuerier querier, + IGame game, + string identifier) { - List releases = querier.AvailableByIdentifier(identifier).ToList(); - if (releases != null && releases.Count > 0) { - CkanModule.GetMinMaxVersions(releases, out _, out _, out GameVersion minKsp, out GameVersion maxKsp); + List releases = null; + try + { + releases = querier.AvailableByIdentifier(identifier) + .ToList(); + } + catch + { + var instMod = querier.InstalledModule(identifier); + if (instMod != null) + { + releases = Enumerable.Repeat(instMod.Module, 1) + .ToList(); + } + } + if (releases != null && releases.Count > 0) + { + CkanModule.GetMinMaxVersions(releases, out _, out _, + out GameVersion minKsp, out GameVersion maxKsp); return GameVersionRange.VersionSpan(game, minKsp, maxKsp); } return ""; diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index ade1dd948c..6a796a7804 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -740,7 +740,10 @@ private void InstallFromCkanFiles(string[] files) Properties.Resources.AllModVersionsInstallYes, Properties.Resources.AllModVersionsInstallNo)) { - InstallModuleDriver(registry_manager.registry, toInstall); + UpdateChangesDialog(toInstall.Select(m => new ModChange(m, GUIModChangeType.Install)) + .ToList(), + null); + tabController.ShowTab("ChangesetTabPage", 1); } } diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 84c5e2feeb..a1122793e3 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -131,40 +131,45 @@ private void InstallMods(object sender, DoWorkEventArgs e) } Util.Invoke(this, () => UseWaitCursor = true); - // Prompt for recommendations and suggestions, if any - if (installer.FindRecommendations( - changes.Where(ch => ch.ChangeType == GUIModChangeType.Install) - .Select(ch => ch.Mod) - .ToHashSet(), - toInstall, - registry, - out Dictionary>> recommendations, - out Dictionary> suggestions, - out Dictionary> supporters)) + try { - tabController.ShowTab("ChooseRecommendedModsTabPage", 3); - ChooseRecommendedMods.LoadRecommendations( - registry, toInstall, toUninstall, - CurrentInstance.VersionCriteria(), Manager.Cache, - recommendations, suggestions, supporters); - tabController.SetTabLock(true); - Util.Invoke(this, () => UseWaitCursor = false); - var result = ChooseRecommendedMods.Wait(); - tabController.SetTabLock(false); - tabController.HideTab("ChooseRecommendedModsTabPage"); - if (result == null) - { - e.Result = new InstallResult(false, changes); - throw new CancelledActionKraken(); - } - else + // Prompt for recommendations and suggestions, if any + if (installer.FindRecommendations( + changes.Where(ch => ch.ChangeType == GUIModChangeType.Install) + .Select(ch => ch.Mod) + .ToHashSet(), + toInstall, + registry, + out Dictionary>> recommendations, + out Dictionary> suggestions, + out Dictionary> supporters)) { - toInstall = toInstall.Concat(result).Distinct().ToList(); + tabController.ShowTab("ChooseRecommendedModsTabPage", 3); + ChooseRecommendedMods.LoadRecommendations( + registry, toInstall, toUninstall, + CurrentInstance.VersionCriteria(), Manager.Cache, + recommendations, suggestions, supporters); + tabController.SetTabLock(true); + Util.Invoke(this, () => UseWaitCursor = false); + var result = ChooseRecommendedMods.Wait(); + tabController.SetTabLock(false); + tabController.HideTab("ChooseRecommendedModsTabPage"); + if (result == null) + { + e.Result = new InstallResult(false, changes); + throw new CancelledActionKraken(); + } + else + { + toInstall = toInstall.Concat(result).Distinct().ToList(); + } } } - else + finally { + // Make sure the progress tab always shows up with a normal cursor even if an exception is thrown Util.Invoke(this, () => UseWaitCursor = false); + ShowWaitDialog(); } // Now let's make all our changes. @@ -173,7 +178,6 @@ private void InstallMods(object sender, DoWorkEventArgs e) // Need to be on the GUI thread to get the translated string tabController.RenameTab("WaitTabPage", Properties.Resources.MainInstallWaitTitle); }); - ShowWaitDialog(); tabController.SetTabLock(true); IDownloader downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache); diff --git a/GUI/Main/MainWait.cs b/GUI/Main/MainWait.cs index b9473dc64f..bcffa8e1db 100644 --- a/GUI/Main/MainWait.cs +++ b/GUI/Main/MainWait.cs @@ -51,11 +51,13 @@ public void HideWaitDialog() /// Message displayed above the DialogProgress bar public void FailWaitDialog(string statusMsg, string logMsg, string description) { - Util.Invoke(statusStrip1, () => { + Util.Invoke(statusStrip1, () => + { StatusProgress.Visible = false; currentUser.RaiseMessage(statusMsg); }); - Util.Invoke(WaitTabPage, () => { + Util.Invoke(WaitTabPage, () => + { RecreateDialogs(); Wait.Finish(); }); @@ -65,6 +67,7 @@ public void FailWaitDialog(string statusMsg, string logMsg, string description) public void Wait_OnRetry() { + EnableMainWindow(); tabController.ShowTab("ChangesetTabPage", 1); }