Refactored PluginManager to accept load hints.

This commit is contained in:
asdfasdfa
2019-06-17 21:17:00 -07:00
parent ae3d1262f5
commit 649dcf4019
5 changed files with 323 additions and 225 deletions

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace Torch
{
public static class LinqExtensions
{
public static IEnumerable<T> TSort<T>( this IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle = false )
{
var sorted = new List<T>();
var visited = new HashSet<T>();
foreach( var item in source )
Visit( item, visited, sorted, dependencies, throwOnCycle );
return sorted;
}
private static void Visit<T>( T item, HashSet<T> visited, List<T> sorted, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle )
{
if( !visited.Contains( item ) )
{
visited.Add( item );
var resolvedDependencies = dependencies(item);
if (resolvedDependencies != null)
{
foreach (var dep in resolvedDependencies)
Visit(dep, visited, sorted, dependencies, throwOnCycle);
}
sorted.Add( item );
}
else
{
if( throwOnCycle && !sorted.Contains( item ) )
throw new Exception( "Cyclic dependency found" );
}
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace Torch
{
public class PluginDependency
{
/// <summary>
/// A unique identifier for the plugin that identifies the dependency.
/// </summary>
public Guid Plugin { get; set; }
/// <summary>
/// The plugin minimum version. This must include a string in the format of #[.#[.#]] for update checking purposes.
/// </summary>
public string MinVersion { get; set; }
}
}

View File

@@ -25,26 +25,39 @@ namespace Torch.Managers
/// <inheritdoc />
public class PluginManager : Manager, IPluginManager
{
private class PluginItem
{
public string Filename { get; set; }
public string Path { get; set; }
public PluginManifest Manifest { get; set; }
public bool IsZip { get; set; }
public List<PluginItem> ResolvedDependencies { get; set; }
}
private static Logger _log = LogManager.GetCurrentClassLogger();
private const string MANIFEST_NAME = "manifest.xml";
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
private readonly MtObservableSortedDictionary<Guid, ITorchPlugin> _plugins = new MtObservableSortedDictionary<Guid, ITorchPlugin>();
private CommandManager _mgr;
#pragma warning disable 649
[Dependency]
private ITorchSessionManager _sessionManager;
#pragma warning restore 649
/// <inheritdoc />
public IReadOnlyDictionary<Guid, ITorchPlugin> Plugins => _plugins.AsReadOnlyObservable();
public event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{
if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir);
}
/// <summary>
/// Updates loaded plugins in parallel.
/// </summary>
@@ -62,7 +75,7 @@ namespace Torch.Managers
}
}
}
/// <inheritdoc/>
public override void Attach()
{
@@ -70,8 +83,6 @@ namespace Torch.Managers
_sessionManager.SessionStateChanged += SessionManagerOnSessionStateChanged;
}
private CommandManager _mgr;
private void SessionManagerOnSessionStateChanged(ITorchSession session, TorchSessionState newState)
{
_mgr = session.Managers.GetManager<CommandManager>();
@@ -93,7 +104,7 @@ namespace Torch.Managers
return;
}
}
/// <summary>
/// Unloads all plugins.
/// </summary>
@@ -108,18 +119,95 @@ namespace Torch.Managers
public void LoadPlugins()
{
bool firstLoad = Torch.Config.Plugins.Count == 0;
List<Guid> foundPlugins = new List<Guid>();
if (Torch.Config.ShouldUpdatePlugins)
DownloadPluginUpdates();
_log.Info("Loading plugins...");
var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
var pluginItems = GetLocalPlugins(PluginDir);
var pluginsToLoad = new List<PluginItem>();
foreach (var item in pluginItems)
{
var pluginItem = item;
if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins))
{
// We have some missing dependencies.
// Future fix would be to download them, but instead for now let's
// just warn the user it's missing
foreach(var missingPlugin in missingPlugins)
_log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin.");
continue;
}
pluginsToLoad.Add(pluginItem);
}
// Download any plugin updates.
bool updatesGotten = DownloadPluginUpdates(pluginsToLoad);
if (updatesGotten)
{
// Resort the plugins just in case updates changed load hints.
pluginItems = GetLocalPlugins(PluginDir);
pluginsToLoad.Clear();
foreach (var item in pluginItems)
{
var pluginItem = item;
if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins))
{
foreach (var missingPlugin in missingPlugins)
_log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin.");
continue;
}
pluginsToLoad.Add(pluginItem);
}
}
// Sort based on dependencies.
try
{
pluginsToLoad = pluginsToLoad.TSort(item => item.ResolvedDependencies)
.ToList();
// Actually load the plugins now.
foreach (var item in pluginsToLoad)
{
LoadPlugin(item);
}
}
catch (Exception e)
{
// This will happen on cylic dependencies.
_log.Error(e);
}
}
private List<PluginItem> GetLocalPlugins(string pluginDir)
{
var firstLoad = Torch.Config.Plugins.Count == 0;
var pluginItems = Directory.EnumerateFiles(pluginDir, "*.zip")
.Union(Directory.EnumerateDirectories(PluginDir));
var results = new List<PluginItem>();
foreach (var item in pluginItems)
{
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping.");
continue;
}
var duplicatePlugin = results.FirstOrDefault(r => r.Manifest.Guid == manifest.Guid);
if (duplicatePlugin != null)
{
_log.Warn(
$"The GUID provided by {manifest.Name} ({item}) is already in use by {duplicatePlugin.Manifest.Name}.");
continue;
}
if (!Torch.Config.LocalPlugins)
{
if (isZip && !Torch.Config.Plugins.Contains(manifest.Guid))
@@ -132,126 +220,39 @@ namespace Torch.Managers
_log.Info($"First-time load: Plugin {manifest.Name} added to torch.cfg.");
Torch.Config.Plugins.Add(manifest.Guid);
}
if(isZip)
foundPlugins.Add(manifest.Guid);
}
LoadPlugin(item);
}
if (!Torch.Config.LocalPlugins && firstLoad)
Torch.Config.Save();
if (!Torch.Config.LocalPlugins)
{
List<string> toLoad = new List<string>();
//This is actually the easiest way to batch process async tasks and block until completion (????)
Task.WhenAll(Torch.Config.Plugins.Select(async g =>
{
try
{
if (foundPlugins.Contains(g))
{
return;
}
var item = await PluginQuery.Instance.QueryOne(g);
string s = Path.Combine(PluginDir, item.Name + ".zip");
await PluginQuery.Instance.DownloadPlugin(item, s);
lock (toLoad)
toLoad.Add(s);
}
catch (Exception ex)
{
_log.Error(ex);
}
}));
foreach (var l in toLoad)
results.Add(new PluginItem
{
LoadPlugin(l);
}
Filename = item,
IsZip = isZip,
Manifest = manifest,
Path = path
});
}
//just reuse the list from earlier
foundPlugins.Clear();
foreach (var plugin in _plugins.Values)
{
try
{
plugin.Init(Torch);
}
catch (Exception e)
{
_log.Error(e, $"Plugin {plugin.Name} threw an exception during init! Unloading plugin!");
foundPlugins.Add(plugin.Id);
}
}
foreach (var id in foundPlugins)
{
var p = _plugins[id];
_plugins.Remove(id);
_mgr.UnregisterPluginCommands(p);
p.Dispose();
}
_log.Info($"Loaded {_plugins.Count} plugins.");
PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
}
private void LoadPlugin(string item)
{
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping.");
return;
}
if (_plugins.ContainsKey(manifest.Guid))
{
_log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {_plugins[manifest.Guid].Name}");
return;
}
if (isZip)
LoadPluginFromZip(path);
else
LoadPluginFromFolder(path);
}
private void DownloadPluginUpdates()
return results;
}
private bool DownloadPluginUpdates(List<PluginItem> plugins)
{
_log.Info("Checking for plugin updates...");
var count = 0;
var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip");
Task.WhenAll(pluginItems.Select(async item =>
Task.WhenAll(plugins.Select(async item =>
{
PluginManifest manifest = null;
try
{
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
if (!isZip)
if (!item.IsZip)
{
_log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}");
return;
}
manifest = GetManifestFromZip(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
return;
}
manifest.Version.TryExtractVersion(out Version currentVersion);
var latest = await PluginQuery.Instance.QueryOne(manifest.Guid);
item.Manifest.Version.TryExtractVersion(out Version currentVersion);
var latest = await PluginQuery.Instance.QueryOne(item.Manifest.Guid);
if (latest?.LatestVersion == null)
{
_log.Warn($"Plugin {manifest.Name} does not have any releases on torchapi.net. Cannot update.");
_log.Warn($"Plugin {item.Manifest.Name} does not have any releases on torchapi.net. Cannot update.");
return;
}
@@ -259,115 +260,107 @@ namespace Torch.Managers
if (currentVersion == null || newVersion == null)
{
_log.Error($"Error parsing version from manifest or website for plugin '{manifest.Name}.'");
_log.Error($"Error parsing version from manifest or website for plugin '{item.Manifest.Name}.'");
return;
}
if (newVersion <= currentVersion)
{
_log.Debug($"{manifest.Name} {manifest.Version} is up to date.");
_log.Debug($"{item.Manifest.Name} {item.Manifest.Version} is up to date.");
return;
}
_log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {newVersion}.");
await PluginQuery.Instance.DownloadPlugin(latest, path);
_log.Info($"Updating plugin '{item.Manifest.Name}' from {currentVersion} to {newVersion}.");
await PluginQuery.Instance.DownloadPlugin(latest, item.Path);
Interlocked.Increment(ref count);
}
catch (Exception e)
{
_log.Warn($"An error occurred updating the plugin {manifest?.Name ?? item}.");
_log.Warn($"An error occurred updating the plugin {item.Manifest.Name}.");
_log.Warn(e);
}
}));
_log.Info($"Updated {count} plugins.");
return count > 0;
}
private void LoadPluginFromFolder(string directory)
private void LoadPlugin(PluginItem item)
{
var assemblies = new List<Assembly>();
var files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToList();
var manifest = GetManifestFromDirectory(directory);
if (manifest == null)
if (item.IsZip)
{
_log.Warn($"Directory {directory} is missing a manifest, skipping load.");
return;
}
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = File.OpenRead(file))
using (var zipFile = ZipFile.OpenRead(item.Path))
{
var data = stream.ReadToEnd();
byte[] symbol = null;
var symbolPath = Path.Combine(Path.GetDirectoryName(file) ?? ".",
Path.GetFileNameWithoutExtension(file) + ".pdb");
if (File.Exists(symbolPath))
try
{
using (var symbolStream = File.OpenRead(symbolPath))
symbol = symbolStream.ReadToEnd();
}
catch (Exception e)
{
_log.Warn(e, $"Failed to read debugging symbols from {symbolPath}");
}
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
}
}
RegisterAllAssemblies(assemblies);
InstantiatePlugin(manifest, assemblies);
}
private void LoadPluginFromZip(string path)
{
PluginManifest manifest;
var assemblies = new List<Assembly>();
using (var zipFile = ZipFile.OpenRead(path))
{
manifest = GetManifestFromZip(path);
if (manifest == null)
{
_log.Warn($"Zip file {path} is missing a manifest, skipping.");
return;
}
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = entry.Open())
foreach (var entry in zipFile.Entries)
{
var data = stream.ReadToEnd((int)entry.Length);
byte[] symbol = null;
var symbolEntryName = entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb";
var symbolEntry = zipFile.GetEntry(symbolEntryName);
if (symbolEntry != null)
try
{
using (var symbolStream = symbolEntry.Open())
symbol = symbolStream.ReadToEnd((int)symbolEntry.Length);
}
catch (Exception e)
{
_log.Warn(e, $"Failed to read debugging symbols from {path}:{symbolEntryName}");
}
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
if (!entry.Name.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = entry.Open())
{
var data = stream.ReadToEnd((int) entry.Length);
byte[] symbol = null;
var symbolEntryName =
entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb";
var symbolEntry = zipFile.GetEntry(symbolEntryName);
if (symbolEntry != null)
try
{
using (var symbolStream = symbolEntry.Open())
symbol = symbolStream.ReadToEnd((int) symbolEntry.Length);
}
catch (Exception e)
{
_log.Warn(e, $"Failed to read debugging symbols from {item.Filename}:{symbolEntryName}");
}
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
}
}
}
}
else
{
var files = Directory
.EnumerateFiles(item.Path, "*.*", SearchOption.AllDirectories)
.ToList();
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = File.OpenRead(file))
{
var data = stream.ReadToEnd();
byte[] symbol = null;
var symbolPath = Path.Combine(Path.GetDirectoryName(file) ?? ".",
Path.GetFileNameWithoutExtension(file) + ".pdb");
if (File.Exists(symbolPath))
try
{
using (var symbolStream = File.OpenRead(symbolPath))
symbol = symbolStream.ReadToEnd();
}
catch (Exception e)
{
_log.Warn(e, $"Failed to read debugging symbols from {symbolPath}");
}
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
}
}
}
RegisterAllAssemblies(assemblies);
InstantiatePlugin(manifest, assemblies);
InstantiatePlugin(item.Manifest, assemblies);
}
private void RegisterAllAssemblies(IReadOnlyCollection<Assembly> assemblies)
{
Assembly ResolveDependentAssembly(object sender, ResolveEventArgs args)
@@ -395,38 +388,12 @@ namespace Torch.Managers
TorchBase.RegisterAuxAssembly(asm);
}
}
private static bool IsAssemblyCompatible(AssemblyName a, AssemblyName b)
{
return a.Name == b.Name && a.Version.Major == b.Version.Major && a.Version.Minor == b.Version.Minor;
}
private PluginManifest GetManifestFromZip(string path)
{
using (var zipFile = ZipFile.OpenRead(path))
{
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = new StreamReader(entry.Open()))
{
return PluginManifest.Load(stream);
}
}
}
return null;
}
private PluginManifest GetManifestFromDirectory(string directory)
{
var path = Path.Combine(directory, MANIFEST_NAME);
return !File.Exists(path) ? null : PluginManifest.Load(path);
}
private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> assemblies)
{
Type pluginType = null;
@@ -495,6 +462,74 @@ namespace Torch.Managers
plugin.Torch = Torch;
_plugins.Add(manifest.Guid, plugin);
}
private PluginManifest GetManifestFromZip(string path)
{
using (var zipFile = ZipFile.OpenRead(path))
{
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = new StreamReader(entry.Open()))
{
return PluginManifest.Load(stream);
}
}
}
return null;
}
private bool TryValidatePluginDependencies(List<PluginItem> items, ref PluginItem item, out List<Guid> missingDependencies)
{
var dependencies = new List<PluginItem>();
missingDependencies = new List<Guid>();
foreach (var pluginDependency in item.Manifest.Dependencies)
{
var dependency = items
.FirstOrDefault(pi => pi?.Manifest.Guid == pluginDependency.Plugin);
if (dependency == null)
{
missingDependencies.Add(pluginDependency.Plugin);
continue;
}
if (!string.IsNullOrEmpty(pluginDependency.MinVersion)
&& dependency.Manifest.Version.TryExtractVersion(out var dependencyVersion)
&& pluginDependency.MinVersion.TryExtractVersion(out var minVersion))
{
// really only care about version if it is defined.
if (dependencyVersion < minVersion)
{
// If dependency version is too low, we can try to update. Otherwise
// it's a missing dependency.
// For now let's just warn the user. bitMuse is lazy.
_log.Warn($"{dependency.Manifest.Name} is below the requested version for {item.Manifest.Name}."
+ Environment.NewLine
+ $" Desired version: {pluginDependency.MinVersion}, Available version: {dependency.Manifest.Version}");
missingDependencies.Add(pluginDependency.Plugin);
continue;
}
}
dependencies.Add(dependency);
}
item.ResolvedDependencies = dependencies;
if (missingDependencies.Count > 0)
return false;
return true;
}
private PluginManifest GetManifestFromDirectory(string directory)
{
var path = Path.Combine(directory, MANIFEST_NAME);
return !File.Exists(path) ? null : PluginManifest.Load(path);
}
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
public IEnumerator<ITorchPlugin> GetEnumerator()

View File

@@ -37,7 +37,7 @@ namespace Torch
/// <summary>
/// A list of dependent plugin repositories. This may be updated to include GUIDs in the future.
/// </summary>
public List<string> Dependencies { get; } = new List<string>();
public List<PluginDependency> Dependencies { get; } = new List<PluginDependency>();
public void Save(string path)
{

View File

@@ -168,9 +168,12 @@
<HintPath>..\GameBinaries\VRage.OpenVRWrapper.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Platform.Windows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Reference Include="VRage.Platform.Windows, Culture=neutral, PublicKeyToken=null">
<HintPath>..\bin\x64\Release\DedicatedServer64\VRage.Platform.Windows.dll</HintPath>
</Reference>
<Reference Include="VRage.Platform.Windows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\GameBinaries\VRage.Platform.Windows.dll</HintPath>
</Reference>
<Reference Include="VRage.Render">
<HintPath>..\GameBinaries\VRage.Render.dll</HintPath>
<Private>False</Private>
@@ -203,6 +206,7 @@
<Compile Include="Collections\TransformEnumerator.cs" />
<Compile Include="Commands\ConsoleCommandContext.cs" />
<Compile Include="Event\EventShimAttribute.cs" />
<Compile Include="Extensions\LinqExtensions.cs" />
<Compile Include="Managers\ChatManager\ChatManagerClient.cs" />
<Compile Include="Managers\ChatManager\ChatManagerServer.cs" />
<Compile Include="Extensions\DispatcherExtensions.cs" />
@@ -242,6 +246,7 @@
<Compile Include="Patches\PhysicsMemoryPatch.cs" />
<Compile Include="Patches\SessionDownloadPatch.cs" />
<Compile Include="Patches\TorchAsyncSaving.cs" />
<Compile Include="Plugins\PluginDependency.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Collections\KeyTree.cs" />
<Compile Include="Collections\RollingAverage.cs" />