Skip to content

Commit

Permalink
PluginManager refactor v2 (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tides committed May 4, 2024
1 parent 75ff77d commit 59596d7
Show file tree
Hide file tree
Showing 26 changed files with 659 additions and 704 deletions.
10 changes: 10 additions & 0 deletions Obsidian.API/Plugins/IPluginContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Obsidian.API.Plugins;
public interface IPluginContainer
{
/// <summary>
/// Searches for the specified file that was packed alongside your plugin.
/// </summary>
/// <param name="fileName">The name of the file you're searching for.</param>
/// <returns>Null if the file is not found or the byte array of the file.</returns>
public byte[]? GetFileData(string fileName);
}
11 changes: 11 additions & 0 deletions Obsidian.API/Plugins/IPluginInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,15 @@ public interface IPluginInfo
public string Description { get; }
public string[] Authors { get; }
public Uri ProjectUrl { get; }

public PluginDependency[] Dependencies { get; }
}

public readonly struct PluginDependency
{
public required string Id { get; init; }

public required string Version { get; init; }

public bool Required { get; init; }
}
30 changes: 11 additions & 19 deletions Obsidian.API/Plugins/PluginBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,16 @@ namespace Obsidian.API.Plugins;
/// </summary>
public abstract class PluginBase : IDisposable, IAsyncDisposable
{
#nullable disable
public IPluginInfo Info { get; internal set; }
public IPluginInfo Info { get; internal set; } = default!;

internal Action unload;
#nullable restore

private Type typeCache;

public PluginBase()
{
typeCache ??= GetType();
}
public IPluginContainer Container { get; internal set; } = default!;

/// <summary>
/// Used for registering services.
/// </summary>
/// <remarks>
/// Only services from the Server will be injected when this method is called. e.x (ILogger, IServerConfiguration).
/// Services registered through this method will be availiable/injected when <seealso cref="OnLoadAsync(IServer)"/> is called.
/// Services registered through this method will be availiable/injected when <seealso cref="OnServerReadyAsync(IServer)"/> is called.
/// </remarks>
public virtual void ConfigureServices(IServiceCollection services) { }

Expand All @@ -35,24 +26,25 @@ public PluginBase()
/// <param name="pluginRegistry"></param>
/// <remarks>
/// Services from the Server will be injected when this method is called. e.x (ILogger, IServerConfiguration).
/// Services registered through this method will be availiable/injected when <seealso cref="OnLoadAsync(IServer)"/> is called.
/// Services registered through this method will be availiable/injected when <seealso cref="OnServerReadyAsync(IServer)"/> is called.
/// </remarks>
public virtual void ConfigureRegistry(IPluginRegistry pluginRegistry) { }


/// <summary>
/// Called when the world has loaded and the server is joinable.
/// </summary>
public virtual ValueTask OnLoadAsync(IServer server) => ValueTask.CompletedTask;
public virtual ValueTask OnServerReadyAsync(IServer server) => ValueTask.CompletedTask;

/// <summary>
/// Called when the plugin has fully loaded.
/// </summary>
public virtual ValueTask OnLoadedAsync(IServer server) => ValueTask.CompletedTask;

/// <summary>
/// Causes this plugin to be unloaded.
/// Called when the plugin is being unloaded.
/// </summary>
protected void Unload()
{
unload();
}
public virtual ValueTask OnUnloadingAsync() => ValueTask.CompletedTask;

public override sealed bool Equals(object? obj) => base.Equals(obj);
public override sealed int GetHashCode() => base.GetHashCode();
Expand Down
5 changes: 5 additions & 0 deletions Obsidian.API/_Interfaces/IServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public interface IServerConfiguration
/// </summary>
public bool CanThrottle => this.ConnectionThrottle > 0;

/// <summary>
/// Determines where or not the server should load plugins that don't have a valid signature.
/// </summary>
public bool AllowUntrustedPlugins { get; set; }

/// <summary>
/// Allows the server to advertise itself as a LAN server to devices on your network.
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Logging\**" />
<EmbeddedResource Remove="Logging\**" />
<None Remove="Logging\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
Expand All @@ -18,7 +24,9 @@
</ItemGroup>

<ItemGroup>
<Folder Include="Logging\" />
<None Update="accepted_keys\obsidian.pub.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions Obsidian.ConsoleApp/accepted_keys/obsidian.pub.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<RSAKeyValue>
<Modulus>AMYF4iNy8WYQbwlNFxKiEwkjx4TobuMjIgIXgQ4FOBKYVKC77DKOlQ8DKaGn3jOufOZ+Q/+Wgvs28WQjwCOFA9hZJwlYpAGKjnaVGAIcCIU1aCNS6whfP3y/oBB94c+qbLtXXaUdo9qszGTXuYFnyb+GnGCxkdK0N3K6NiTs57xii1VqunwQUlod8+ULo6JbJFRmmlnqzBdYuQNDMpFoLnCq3NZvRJxNe4PP89M3bS2UjS5H1ZM86nTVg9oO4yKMLX4MORlVLWEvP2lvbg1Mrg4fveuPLhMkGZDnvmWaatXxlMoUDv8wQ4+PGrcrhtXS4OqkPl7qFQe9eS4K859kv+NyLDYrb1dtfV7wBZHF5oPnbwyUWgDfT3SWxixY1FqGNEDSRtpo9mUzJvRnvG3V/hNt2YtThpZrxRpZYPoLqiVrKT87vARbFWNjX2QO14XAk/fSqwtzCvF5p/EQl6VuoQ9ylC+2KmAk3U5exydaMw6jksGvVkR7c6lj9J6AZ4nWvw==</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
4 changes: 2 additions & 2 deletions Obsidian/Obsidian.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.NetCoreSdk" Version="1.9.7" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
Expand Down
14 changes: 4 additions & 10 deletions Obsidian/Plugins/DirectoryWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ public sealed class DirectoryWatcher : IDisposable
private string[] _filters = [];
public string[] Filters { get => _filters; set => _filters = value ?? []; }

public event Action<string> FileChanged;
public event Action<string, string> FileRenamed;
public event Action<string> FileDeleted;
public event Action<string> FileChanged = default!;
public event Action<string, string> FileRenamed = default!;
public event Action<string> FileDeleted = default!;

private readonly Dictionary<string, FileSystemWatcher> watchers = new();
private readonly Dictionary<string, DateTime> updateTimestamps = new();
Expand All @@ -21,12 +21,6 @@ public void Watch(string path)
if (!Directory.Exists(path))
throw new DirectoryNotFoundException(path);

foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
if (TestFilter(file))
FileChanged?.Invoke(file);
}

lock (watchers)
{
if (watchers.ContainsKey(path))
Expand Down Expand Up @@ -56,7 +50,7 @@ public void Unwatch(string path)
{
lock (watchers)
{
if (!watchers.TryGetValue(path, out FileSystemWatcher watcher))
if (!watchers.TryGetValue(path, out FileSystemWatcher? watcher))
throw new KeyNotFoundException();

watcher.Created -= OnFileUpdated;
Expand Down
97 changes: 70 additions & 27 deletions Obsidian/Plugins/PluginContainer.cs
Original file line number Diff line number Diff line change
@@ -1,59 +1,84 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Obsidian.API.Plugins;
using Obsidian.Plugins.PluginProviders;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Loader;

namespace Obsidian.Plugins;

public sealed class PluginContainer : IDisposable
public sealed class PluginContainer : IDisposable, IPluginContainer
{
private Type? pluginType;
private bool initialized;

private Type? PluginType => this.Plugin?.GetType();

public IServiceScope ServiceScope { get; internal set; } = default!;
public PluginInfo Info { get; private set; } = default!;

public PluginBase? Plugin { get; internal set; }

[AllowNull]
public PluginLoadContext LoadContext { get; internal set; } = default!;

[AllowNull]
public PluginBase Plugin { get; private set; }
public PluginInfo Info { get; }
public Assembly PluginAssembly { get; internal set; } = default!;

[AllowNull]
public AssemblyLoadContext LoadContext { get; private set; }
public Assembly PluginAssembly { get; } = default!;
public FrozenDictionary<string, PluginFileEntry> FileEntries { get; internal set; } = default!;

public string Source { get; internal set; } = default!;
public string ClassName { get; } = default!;
public required string Source { get; set; }
public required bool ValidSignature { get; init; }

public bool HasDependencies { get; private set; } = true;
public bool IsReady => HasDependencies;
public bool Loaded { get; internal set; }

public PluginContainer(PluginInfo info, string source)
~PluginContainer()
{
Info = info;
Source = source;
this.Dispose(false);
}

public PluginContainer(PluginBase plugin, PluginInfo info, Assembly assembly, AssemblyLoadContext loadContext, string source)
internal void Initialize()
{
if (!this.initialized)
{
var pluginJsonData = this.GetFileData("plugin.json") ?? throw new InvalidOperationException("Failed to find plugin.json");

this.Info = pluginJsonData.FromJson<PluginInfo>() ?? throw new NullReferenceException("Failed to deserialize plugin.json");

Plugin = plugin;
Info = info;
LoadContext = loadContext;
Source = source;
PluginAssembly = assembly;
this.initialized = true;

pluginType = plugin.GetType();
ClassName = pluginType.Name;
Plugin.Info = Info;
return;
}

this.Plugin!.Container = this;
this.Plugin!.Info = this.Info;
}

//TODO PLUGINS SHOULD USE VERSION CLASS TO SPECIFY VERSION
internal bool IsDependency(string pluginId) =>
this.Info.Dependencies.Any(x => x.Id == pluginId);

internal bool AddDependency(PluginLoadContext pluginLoadContext)
{
ArgumentNullException.ThrowIfNull(pluginLoadContext);

if (this.LoadContext == null)
return false;

this.LoadContext.AddDependency(pluginLoadContext);
return true;
}

/// <summary>
/// Inject the scoped services into
/// </summary>
public void InjectServices(ILogger? logger, object? target = null)
internal void InjectServices(ILogger? logger, object? target = null)
{
var properties = target is null ? this.pluginType!.WithInjectAttribute() : target.GetType().WithInjectAttribute();
var properties = target is null ? this.PluginType!.WithInjectAttribute() : target.GetType().WithInjectAttribute();

target ??= this.Plugin;

Expand All @@ -70,15 +95,33 @@ public void InjectServices(ILogger? logger, object? target = null)
logger?.LogError(ex, "Failed to inject service.");
}
}
}

///<inheritdoc/>
public byte[]? GetFileData(string fileName)
{
var fileEntry = this.FileEntries?.GetValueOrDefault(fileName);

return fileEntry?.GetData();
}

public void Dispose()
{
Plugin = null;
LoadContext = null;
pluginType = null;
this.Dispose(true);

this.ServiceScope.Dispose();
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
this.ServiceScope?.Dispose();

if (disposing)
{
this.PluginAssembly = null;
this.LoadContext = null;
this.Plugin = null;
this.FileEntries = null;
}
}
}
33 changes: 33 additions & 0 deletions Obsidian/Plugins/PluginFileEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Org.BouncyCastle.Crypto;
using System.IO;
using System.IO.Compression;

namespace Obsidian.Plugins;
public sealed class PluginFileEntry
{
internal byte[] rawData = default!;

public required string Name { get; init; }

public required int Length { get; init; }

public required int CompressedLength { get; init; }

public required int Offset { get; set; }

public bool IsCompressed => Length != CompressedLength;

internal byte[] GetData()
{
if (!this.IsCompressed)
return this.rawData;

using var ms = new MemoryStream(this.rawData, false);
using var ds = new DeflateStream(ms, CompressionMode.Decompress);
using var outStream = new MemoryStream();

ds.CopyTo(outStream);

return outStream.Length != this.Length ? throw new DataLengthException() : outStream.ToArray();
}
}
7 changes: 0 additions & 7 deletions Obsidian/Plugins/PluginInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,3 @@ internal PluginInfo(string name)
Authors = ["Unknown"];
}
}

public readonly struct PluginDependency
{
public required string Id { get; init; }

public required Version Version { get; init; }
}

0 comments on commit 59596d7

Please sign in to comment.