Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PluginManager refactor v2 #436

Merged
merged 21 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 virtual void ConfigureServices(IServiceCollection services) { }
/// <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; }
}
Loading
Loading