From f2d74bb392da5c5b2f3312eef6b0db542e108fc6 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Fri, 20 Dec 2019 20:47:05 -0600 Subject: [PATCH] Added IgnoreReason to ignored plugins Added checks for dependencies during load for when features deny loading --- IPA.Loader/Loader/PluginLoader.cs | 175 ++++++++++++++++++------ IPA.Loader/Loader/PluginManager.cs | 4 +- IPA.Loader/Loader/PluginManifest.cs | 6 +- IPA.Loader/Updating/BeatMods/Updater.cs | 2 +- 4 files changed, 139 insertions(+), 48 deletions(-) diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index 984fa3ee..6a8051b3 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -12,6 +12,7 @@ using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using Version = SemVer.Version; +using SemVer; #if NET4 using Task = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task; @@ -267,14 +268,18 @@ namespace IPA.Loader { Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); Logger.loader.Error(e); - ignoredPlugins.Add(metadata); + ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error) + { + ReasonText = "An error ocurred loading the data", + Error = e + }); } } IEnumerable bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json"); bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest")); foreach (var manifest in bareManifests) - { + { // TODO: maybe find a way to allow a bare manifest to specify an associated file try { var metadata = new PluginMetadata @@ -323,16 +328,13 @@ namespace IPA.Loader continue; } - using (var reader = new StreamReader(resc.GetResourceStream())) - description = reader.ReadToEnd(); + using var reader = new StreamReader(resc.GetResourceStream()); + description = reader.ReadToEnd(); } else { - using (var descriptionReader = - new StreamReader( - meta.Assembly.GetManifestResourceStream(name) ?? - throw new InvalidOperationException())) - description = descriptionReader.ReadToEnd(); + using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name)); + description = descriptionReader.ReadToEnd(); } meta.Manifest.Description = description; @@ -340,16 +342,39 @@ namespace IPA.Loader } } + internal enum Reason + { + Error, Duplicate, Conflict, Dependency, + Released, + Feature + } + internal struct IgnoreReason + { + + public Reason Reason { get; } + public string ReasonText { get; set; } + public Exception Error { get; set; } + public PluginMetadata RelatedTo { get; set; } + public IgnoreReason(Reason reason) + { + Reason = reason; + ReasonText = null; + Error = null; + RelatedTo = null; + } + } + // keep track of these for the updater; it should still be able to update mods not loaded // TODO: add ignore reason - internal static HashSet ignoredPlugins = new HashSet(); + // the thing -> the reason + internal static Dictionary ignoredPlugins = new Dictionary(); internal static void Resolve() { // resolves duplicates and conflicts, etc PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version)); var ids = new HashSet(); - var ignore = new HashSet(); + var ignore = new Dictionary(); var resolved = new List(PluginsMetadata.Count); foreach (var meta in PluginsMetadata) { @@ -358,15 +383,20 @@ namespace IPA.Loader if (ids.Contains(meta.Id)) { Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); - ignore.Add(meta); - ignoredPlugins.Add(meta); + var ireason = new IgnoreReason(Reason.Duplicate) + { + ReasonText = $"Duplicate entry of same ID ({meta.Id})", + RelatedTo = resolved.First(p => p.Id == meta.Id) + }; + ignore.Add(meta, ireason); + ignoredPlugins.Add(meta, ireason); continue; // because of sorted order, hightest order will always be the first one } bool processedLater = false; foreach (var meta2 in PluginsMetadata) { - if (ignore.Contains(meta2)) continue; + if (ignore.ContainsKey(meta2)) continue; if (meta == meta2) { processedLater = true; @@ -378,25 +408,33 @@ namespace IPA.Loader var range = meta2.Manifest.Conflicts[meta.Id]; if (!range.IsSatisfied(meta.Version)) continue; - Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}"); + Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}"); if (processedLater) { Logger.loader.Warn($"Ignoring {meta2.Name}"); - ignore.Add(meta2); + ignore.Add(meta2, new IgnoreReason(Reason.Conflict) + { + ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}", + RelatedTo = meta + }); } else { Logger.loader.Warn($"Ignoring {meta.Name}"); - ignore.Add(meta); + ignore.Add(meta, new IgnoreReason(Reason.Conflict) + { + ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}", + RelatedTo = meta2 + }); break; } } } - if (ignore.Contains(meta)) + if (ignore.TryGetValue(meta, out var reason)) { - ignoredPlugins.Add(meta); + ignoredPlugins.Add(meta, reason); continue; } if (meta.Id != null) @@ -430,7 +468,7 @@ namespace IPA.Loader Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); #endif - bool InsertInto(HashSet root, PluginMetadata meta, bool isRoot = false) + static bool InsertInto(HashSet root, PluginMetadata meta, bool isRoot = false) { // this is slow, and hella recursive bool inserted = false; foreach (var sr in root) @@ -469,7 +507,7 @@ namespace IPA.Loader foreach (var meta in PluginsMetadata) InsertInto(pluginTree, meta, true); - void DeTree(List into, HashSet tree) + static void DeTree(List into, HashSet tree) { foreach (var st in tree) if (!into.Contains(st)) @@ -494,8 +532,7 @@ namespace IPA.Loader var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version); foreach (var meta in PluginsMetadata) { - bool load = true; - bool disable = false; + var missingDeps = new List<(string id, Range version, bool disabled)>(); foreach (var dep in meta.Manifest.Dependencies) { #if DEBUG @@ -504,32 +541,36 @@ namespace IPA.Loader if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) continue; - load = false; - if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key])) { - disable = true; Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too."); + missingDeps.Add((dep.Key, dep.Value, true)); } else + { Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}"); - - break; + missingDeps.Add((dep.Key, dep.Value, false)); + } } - if (load) + if (missingDeps.Count == 0) { metadata.Add(meta); if (meta.Id != null) pluginsToLoad.Add(meta.Id, meta.Version); } - else if (disable) + else if (missingDeps.Any(t => !t.disabled)) + { // missing deps + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}" + }); + } + else { DisabledPlugins.Add(meta); DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name); } - else - ignoredPlugins.Add(meta); } PluginsMetadata = metadata; @@ -594,12 +635,12 @@ namespace IPA.Loader internal static void ReleaseAll(bool full = false) { if (full) - ignoredPlugins = new HashSet(); + ignoredPlugins = new Dictionary(); else { foreach (var m in PluginsMetadata) - ignoredPlugins.Add(m); - foreach (var m in ignoredPlugins) + ignoredPlugins.Add(m, new IgnoreReason(Reason.Released)); + foreach (var m in ignoredPlugins.Keys) { // clean them up so we can still use the metadata for updates m.InternalFeatures.Clear(); m.PluginType = null; @@ -618,7 +659,7 @@ namespace IPA.Loader meta.Assembly = Assembly.LoadFrom(meta.File.FullName); } - internal static PluginInfo InitPlugin(PluginMetadata meta) + internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable alreadyLoaded) { if (meta.PluginType == null) return new PluginInfo() @@ -633,7 +674,33 @@ namespace IPA.Loader Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly."); try - { // TODO: add dependency checking for when features prevent loading + { + foreach (var dep in meta.Dependencies) + { + if (alreadyLoaded.Contains(dep)) continue; + + // otherwise... + + if (ignoredPlugins.TryGetValue(dep, out var reason)) + { // was added to the ignore list + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}", + RelatedTo = dep + }); + } + else + { // was not added to ignore list + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Dependency was not already loaded at load time, but was also not ignored", + RelatedTo = dep + }); + } + + return null; + } + Load(meta); Feature denyingFeature = null; @@ -641,7 +708,10 @@ namespace IPA.Loader { Logger.loader.Warn( $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}"); - ignoredPlugins.Add(meta); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) + { + ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}" + }); return null; } @@ -659,7 +729,10 @@ namespace IPA.Loader { Logger.loader.Warn( $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}"); - ignoredPlugins.Add(meta); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) + { + ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}" + }); return null; } @@ -689,7 +762,8 @@ namespace IPA.Loader } catch (AmbiguousMatchException) { - Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})"); + Logger.loader.Critical($"Only one Init allowed per plugin (ambiguous match in {meta.Name})"); + Logger.loader.Critical("@Developer: you *really* should fix this"); // not adding to ignoredPlugins here because this should only happen in a development context // if someone fucks this up on release thats on them return null; @@ -697,7 +771,11 @@ namespace IPA.Loader catch (Exception e) { Logger.loader.Error($"Could not init plugin {meta.Name}: {e}"); - ignoredPlugins.Add(meta); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error) + { + ReasonText = "Error ocurred while initializing", + Error = e + }); return null; } @@ -708,7 +786,20 @@ namespace IPA.Loader { InitFeatures(); DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily - return PluginsMetadata.Select(InitPlugin).NonNull().ToList(); + + var list = new List(); + var loaded = new HashSet(); + foreach (var meta in PluginsMetadata) + { + var info = InitPlugin(meta, loaded); + if (info != null) + { + list.Add(info); + loaded.Add(meta); + } + } + + return list; } } } \ No newline at end of file diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 89716052..dfa8c94a 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -191,7 +191,7 @@ namespace IPA.Loader PluginLoader.DisabledPlugins.Remove(plugin); if (runtimeInfo == null) { - runtimeInfo = InitPlugin(plugin); + runtimeInfo = InitPlugin(plugin, AllPlugins.Select(i => i.Metadata)); needsRestart = false; } } @@ -325,7 +325,7 @@ namespace IPA.Loader NoRuntimeEnableFeature.HaveLoadedPlugins = true; var metadataPaths = PluginsMetadata.Select(m => m.File.FullName).ToList(); - var ignoredPaths = ignoredPlugins.Select(m => m.File.FullName).ToList(); + var ignoredPaths = ignoredPlugins.Select(m => m.Key.File.FullName).ToList(); var disabledPaths = DisabledPlugins.Select(m => m.File.FullName).ToList(); //Copy plugins to .cache diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs index a545962e..41fe4837 100644 --- a/IPA.Loader/Loader/PluginManifest.cs +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -36,13 +36,13 @@ namespace IPA.Loader public Dictionary Conflicts = new Dictionary(); [JsonProperty("features", Required = Required.DisallowNull)] - public string[] Features = new string[0]; + public string[] Features = Array.Empty(); [JsonProperty("loadBefore", Required = Required.DisallowNull)] - public string[] LoadBefore = new string[0]; + public string[] LoadBefore = Array.Empty(); [JsonProperty("loadAfter", Required = Required.DisallowNull)] - public string[] LoadAfter = new string[0]; + public string[] LoadAfter = Array.Empty(); [JsonProperty("icon", Required = Required.DisallowNull)] public string IconPath = null; diff --git a/IPA.Loader/Updating/BeatMods/Updater.cs b/IPA.Loader/Updating/BeatMods/Updater.cs index 31c36e3b..ad6bf761 100644 --- a/IPA.Loader/Updating/BeatMods/Updater.cs +++ b/IPA.Loader/Updating/BeatMods/Updater.cs @@ -212,7 +212,7 @@ namespace IPA.Updating.BeatMods } } - foreach (var meta in PluginLoader.ignoredPlugins) + foreach (var meta in PluginLoader.ignoredPlugins.Keys) { // update ignored if (meta.Id != null) { // updatable