Browse Source

Added IgnoreReason to ignored plugins

Added checks for dependencies during load for when features deny loading
pull/46/head
Anairkoen Schno 4 years ago
parent
commit
f2d74bb392
4 changed files with 139 additions and 48 deletions
  1. +133
    -42
      IPA.Loader/Loader/PluginLoader.cs
  2. +2
    -2
      IPA.Loader/Loader/PluginManager.cs
  3. +3
    -3
      IPA.Loader/Loader/PluginManifest.cs
  4. +1
    -1
      IPA.Loader/Updating/BeatMods/Updater.cs

+ 133
- 42
IPA.Loader/Loader/PluginLoader.cs View File

@ -12,6 +12,7 @@ using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Version = SemVer.Version; using Version = SemVer.Version;
using SemVer;
#if NET4 #if NET4
using Task = System.Threading.Tasks.Task; using Task = System.Threading.Tasks.Task;
using TaskEx = 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($"Could not load data for plugin {Path.GetFileName(plugin)}");
Logger.loader.Error(e); Logger.loader.Error(e);
ignoredPlugins.Add(metadata);
ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error)
{
ReasonText = "An error ocurred loading the data",
Error = e
});
} }
} }
IEnumerable<string> bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json"); IEnumerable<string> bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json");
bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest")); bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest"));
foreach (var manifest in bareManifests) foreach (var manifest in bareManifests)
{
{ // TODO: maybe find a way to allow a bare manifest to specify an associated file
try try
{ {
var metadata = new PluginMetadata var metadata = new PluginMetadata
@ -323,16 +328,13 @@ namespace IPA.Loader
continue; continue;
} }
using (>n class="kt">var reader = new StreamReader(resc.GetResourceStream()))
description = reader.ReadToEnd();
using var reader = new StreamReader(resc.GetResourceStream());
description = reader.ReadToEnd();
} }
else 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; 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 // keep track of these for the updater; it should still be able to update mods not loaded
// TODO: add ignore reason // TODO: add ignore reason
internal static HashSet<PluginMetadata> ignoredPlugins = new HashSet<PluginMetadata>();
// the thing -> the reason
internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
internal static void Resolve() internal static void Resolve()
{ // resolves duplicates and conflicts, etc { // resolves duplicates and conflicts, etc
PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version)); PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
var ids = new HashSet<string>(); var ids = new HashSet<string>();
var ignore = new HashSet<PluginMetadata>();
var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
var resolved = new List<PluginMetadata>(PluginsMetadata.Count); var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
foreach (var meta in PluginsMetadata) foreach (var meta in PluginsMetadata)
{ {
@ -358,15 +383,20 @@ namespace IPA.Loader
if (ids.Contains(meta.Id)) if (ids.Contains(meta.Id))
{ {
Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); 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 continue; // because of sorted order, hightest order will always be the first one
} }
bool processedLater = false; bool processedLater = false;
foreach (var meta2 in PluginsMetadata) foreach (var meta2 in PluginsMetadata)
{ {
if (ignore.Contains(meta2)) continue;
if (ignore.ContainsKey(meta2)) continue;
if (meta == meta2) if (meta == meta2)
{ {
processedLater = true; processedLater = true;
@ -378,25 +408,33 @@ namespace IPA.Loader
var range = meta2.Manifest.Conflicts[meta.Id]; var range = meta2.Manifest.Conflicts[meta.Id];
if (!range.IsSatisfied(meta.Version)) continue; 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) if (processedLater)
{ {
Logger.loader.Warn($"Ignoring {meta2.Name}"); 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 else
{ {
Logger.loader.Warn($"Ignoring {meta.Name}"); 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; break;
} }
} }
} }
if (ignore.Contains(meta))
if (ignore.TryGetValue(meta, out var reason))
{ {
ignoredPlugins.Add(meta);
ignoredPlugins.Add(meta, reason);
continue; continue;
} }
if (meta.Id != null) if (meta.Id != null)
@ -430,7 +468,7 @@ namespace IPA.Loader
Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP())); Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
#endif #endif
bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
static bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
{ // this is slow, and hella recursive { // this is slow, and hella recursive
bool inserted = false; bool inserted = false;
foreach (var sr in root) foreach (var sr in root)
@ -469,7 +507,7 @@ namespace IPA.Loader
foreach (var meta in PluginsMetadata) foreach (var meta in PluginsMetadata)
InsertInto(pluginTree, meta, true); InsertInto(pluginTree, meta, true);
void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
{ {
foreach (var st in tree) foreach (var st in tree)
if (!into.Contains(st)) 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); var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
foreach (var meta in PluginsMetadata) 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) foreach (var dep in meta.Manifest.Dependencies)
{ {
#if DEBUG #if DEBUG
@ -504,32 +541,36 @@ namespace IPA.Loader
if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
continue; continue;
load = false;
if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key])) 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."); Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
missingDeps.Add((dep.Key, dep.Value, true));
} }
else else
{
Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}"); 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); metadata.Add(meta);
if (meta.Id != null) if (meta.Id != null)
pluginsToLoad.Add(meta.Id, meta.Version); 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); DisabledPlugins.Add(meta);
DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name); DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
} }
else
ignoredPlugins.Add(meta);
} }
PluginsMetadata = metadata; PluginsMetadata = metadata;
@ -594,12 +635,12 @@ namespace IPA.Loader
internal static void ReleaseAll(bool full = false) internal static void ReleaseAll(bool full = false)
{ {
if (full) if (full)
ignoredPlugins = new HashSet<PluginMetadata>();
ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
else else
{ {
foreach (var m in PluginsMetadata) 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 { // clean them up so we can still use the metadata for updates
m.InternalFeatures.Clear(); m.InternalFeatures.Clear();
m.PluginType = null; m.PluginType = null;
@ -618,7 +659,7 @@ namespace IPA.Loader
meta.Assembly = Assembly.LoadFrom(meta.File.FullName); meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
} }
internal static PluginInfo InitPlugin(PluginMetadata meta)
internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
{ {
if (meta.PluginType == null) if (meta.PluginType == null)
return new PluginInfo() 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."); Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
try 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); Load(meta);
Feature denyingFeature = null; Feature denyingFeature = null;
@ -641,7 +708,10 @@ namespace IPA.Loader
{ {
Logger.loader.Warn( Logger.loader.Warn(
$"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}"); $"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; return null;
} }
@ -659,7 +729,10 @@ namespace IPA.Loader
{ {
Logger.loader.Warn( Logger.loader.Warn(
$"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}"); $"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; return null;
} }
@ -689,7 +762,8 @@ namespace IPA.Loader
} }
catch (AmbiguousMatchException) 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 // not adding to ignoredPlugins here because this should only happen in a development context
// if someone fucks this up on release thats on them // if someone fucks this up on release thats on them
return null; return null;
@ -697,7 +771,11 @@ namespace IPA.Loader
catch (Exception e) catch (Exception e)
{ {
Logger.loader.Error($"Could not init plugin {meta.Name}: {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; return null;
} }
@ -708,7 +786,20 @@ namespace IPA.Loader
{ {
InitFeatures(); InitFeatures();
DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily 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<PluginInfo>();
var loaded = new HashSet<PluginMetadata>();
foreach (var meta in PluginsMetadata)
{
var info = InitPlugin(meta, loaded);
if (info != null)
{
list.Add(info);
loaded.Add(meta);
}
}
return list;
} }
} }
} }

+ 2
- 2
IPA.Loader/Loader/PluginManager.cs View File

@ -191,7 +191,7 @@ namespace IPA.Loader
PluginLoader.DisabledPlugins.Remove(plugin); PluginLoader.DisabledPlugins.Remove(plugin);
if (runtimeInfo == null) if (runtimeInfo == null)
{ {
runtimeInfo = InitPlugin(plugin);
runtimeInfo = InitPlugin(plugin, AllPlugins.Select(i => i.Metadata));
needsRestart = false; needsRestart = false;
} }
} }
@ -325,7 +325,7 @@ namespace IPA.Loader
NoRuntimeEnableFeature.HaveLoadedPlugins = true; NoRuntimeEnableFeature.HaveLoadedPlugins = true;
var metadataPaths = PluginsMetadata.Select(m => m.File.FullName).ToList(); 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(); var disabledPaths = DisabledPlugins.Select(m => m.File.FullName).ToList();
//Copy plugins to .cache //Copy plugins to .cache


+ 3
- 3
IPA.Loader/Loader/PluginManifest.cs View File

@ -36,13 +36,13 @@ namespace IPA.Loader
public Dictionary<string, Range> Conflicts = new Dictionary<string, Range>(); public Dictionary<string, Range> Conflicts = new Dictionary<string, Range>();
[JsonProperty("features", Required = Required.DisallowNull)] [JsonProperty("features", Required = Required.DisallowNull)]
public string[] Features = new string[0];
public string[] Features = Array.Empty<string>();
[JsonProperty("loadBefore", Required = Required.DisallowNull)] [JsonProperty("loadBefore", Required = Required.DisallowNull)]
public string[] LoadBefore = new string[0];
public string[] LoadBefore = Array.Empty<string>();
[JsonProperty("loadAfter", Required = Required.DisallowNull)] [JsonProperty("loadAfter", Required = Required.DisallowNull)]
public string[] LoadAfter = new string[0];
public string[] LoadAfter = Array.Empty<string>();
[JsonProperty("icon", Required = Required.DisallowNull)] [JsonProperty("icon", Required = Required.DisallowNull)]
public string IconPath = null; public string IconPath = null;


+ 1
- 1
IPA.Loader/Updating/BeatMods/Updater.cs View File

@ -212,7 +212,7 @@ namespace IPA.Updating.BeatMods
} }
} }
foreach (var meta in PluginLoader.ignoredPlugins)
foreach (var meta in PluginLoader.ignoredPlugins.Keys)
{ // update ignored { // update ignored
if (meta.Id != null) if (meta.Id != null)
{ // updatable { // updatable


Loading…
Cancel
Save