From d401aff3307f6e2e1641610fdd912b572d6b04c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:10:15 -0400 Subject: rewrite update checks (#551) --- src/SMAPI/Framework/IModMetadata.cs | 18 +-- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 7 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 25 +--- .../Framework/ModUpdateChecking/ModUpdateStatus.cs | 37 ------ src/SMAPI/Program.cs | 143 ++++++++------------- src/SMAPI/StardewModdingAPI.csproj | 1 - 6 files changed, 74 insertions(+), 157 deletions(-) delete mode 100644 src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b71c8056..d3ec0035 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework { @@ -46,11 +46,9 @@ namespace StardewModdingAPI.Framework /// Whether the mod is a content pack. bool IsContentPack { get; } - /// The update status of this mod (if any). - ModUpdateStatus UpdateStatus { get; } + /// The update-check metadata for this mod (if any). + ModEntryModel UpdateCheckData { get; } - /// The preview update status of this mod (if any). - ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -78,13 +76,9 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); - /// Set the update status. - /// The mod update status. - IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); - - /// Set the preview update status. - /// The mod preview update status. - IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); + /// Set the update-check metadata for this mod. + /// The update-check metadata. + IModMetadata SetUpdateData(ModEntryModel data); /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index deb12bdc..3801fac3 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -40,9 +40,12 @@ namespace StardewModdingAPI.Framework.ModData /// Get a semantic remote version for update checks. /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) { - return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 88d2770c..02a77778 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework.ModLoading { @@ -44,11 +44,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod-provided API (if any). public object Api { get; private set; } - /// The update status of this mod (if any). - public ModUpdateStatus UpdateStatus { get; private set; } - - /// The preview update status of this mod (if any). - public ModUpdateStatus PreviewUpdateStatus { get; private set; } + /// The update-check metadata for this mod (if any). + public ModEntryModel UpdateCheckData { get; private set; } /// Whether the mod is a content pack. public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -122,19 +119,11 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the update status. - /// The mod update status. - public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) - { - this.UpdateStatus = updateStatus; - return this; - } - - /// Set the preview update status. - /// The mod preview update status. - public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) + /// Set the update-check metadata for this mod. + /// The update-check metadata. + public IModMetadata SetUpdateData(ModEntryModel data) { - this.PreviewUpdateStatus = previewUpdateStatus; + this.UpdateCheckData = data; return this; } diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs deleted file mode 100644 index efb32aef..00000000 --- a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace StardewModdingAPI.Framework.ModUpdateChecking -{ - /// Update status for a mod. - internal class ModUpdateStatus - { - /********* - ** Accessors - *********/ - /// The version that this mod can be updated to (if any). - public ISemanticVersion Version { get; } - - /// The error checking for updates of this mod (if any). - public string Error { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The version that this mod can be update to. - public ModUpdateStatus(ISemanticVersion version) - { - this.Version = version; - } - - /// Construct an instance. - /// The error checking for updates of this mod. - public ModUpdateStatus(string error) - { - this.Error = error; - } - - /// Construct an instance. - public ModUpdateStatus() - { - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2ee18a29..ccdf98ef 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -8,6 +8,7 @@ using System.Net; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Xna.Framework.Input; @@ -24,7 +25,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -592,14 +592,14 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; - if (response.Error != null) + if (latestStable == null && response.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {response.Error}"); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) { @@ -634,103 +634,72 @@ namespace StardewModdingAPI { HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - // prepare update keys - Dictionary modsByKey = - ( - from mod in mods - where - mod.Manifest?.UpdateKeys != null - && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // fetch results - this.Monitor.Log($" Checking {modsByKey.Count} mod update keys.", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary> updatesByMod = new Dictionary>(); - foreach (var result in results) + // prepare search model + List searchMods = new List(); + foreach (IModMetadata mod in mods) { - IModMetadata mod = result.Mod; - ModInfoModel remoteInfo = result.Info; - - // handle error - if (remoteInfo.Error != null) - { - if (mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - if (mod.PreviewUpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); + if (!mod.HasManifest()) continue; - } - // normalise versions - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); - bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + } - if (!validVersion && mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); - if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) - mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary results = client.GetModInfo(searchMods.ToArray()); - if (!validVersion && !validPreviewVersion) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); + // extract update alerts & errors + var updates = new List>(); + var errors = new StringBuilder(); + foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) + { + // link to update-check data + if (!mod.HasManifest() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) continue; - } - - // compare versions - bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); - bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; + mod.SetUpdateData(result); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); - if (isUpdate) + // handle errors + if (result.Errors != null && result.Errors.Any()) { - if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) - { - updatesByMod[mod] = new Tuple(remoteInfo, isPreviewUpdate); - - if (isPreviewUpdate) - mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); - else - mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); - } + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName} update error: {result.Errors[0]}" + : $" {mod.DisplayName} update errors:\n - {string.Join("\n - ", result.Errors)}" + ); } - } - // set mods to have no updates - foreach (IModMetadata mod in results.Select(item => item.Mod) - .Where(item => !updatesByMod.ContainsKey(item))) - { - mod.SetUpdateStatus(new ModUpdateStatus()); - mod.SetPreviewUpdateStatus(new ModUpdateStatus()); + // parse versions + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + ISemanticVersion latestVersion = result.Version != null + ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) + : null; + ISemanticVersion optionalVersion = result.PreviewVersion != null + ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) + : null; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) + updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); } - // output - if (updatesByMod.Any()) + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) { this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {(entry.Value.Item2 ? entry.Value.Item1.PreviewVersion : entry.Value.Item1.Version)}: {entry.Value.Item1.Url}", LogLevel.Alert); + this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updates) + { + IModMetadata mod = entry.Item1; + ISemanticVersion newVersion = entry.Item2; + string newUrl = entry.Item3; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } } else this.Monitor.Log(" All mods up to date.", LogLevel.Trace); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 916dd053..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,7 +110,6 @@ - -- cgit