Remora / Remora.Plugins

A simple, dynamic plugin system for .NET
GNU Lesser General Public License v3.0
3 stars 2 forks source link

[Discussion] Plugin Remaster #4

Open Foxtrek64 opened 2 years ago

Foxtrek64 commented 2 years ago

After some discussions, we have outlined the following priorities for a plugins rewrite:

public interface IPluginDescriptor
{
    /// <summary>
    /// Gets the name of the plugin. This name should be unique.
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Gets the description of the plugin.
    /// </summary>
    string Description { get; }

    /// <summary>
    /// Gets the version of the plugin.
    /// </summary>
    Version Version { get; }

    /// <summary>
    /// Called when the application host is ready to start the plugin.
    /// </summary>
    Task<Result> StartAsync(CancellationToken ct = default);

    /// <summary>
    /// Called when the application host is shutting down the plugin.
    /// </summary>
    /// <remarks>
    /// A plugin must be prepared to stop at any time.
    /// </remarks>
    Task StopAsync();

A plugin should also implement IDisposable to help clean up dependencies. Plugin developers may also implement IAsyncDisposable if applicable to them.

Plugin lifetime:

Please offer thoughts and ideas. This will be shaped into a proper proposal over time.

AraHaan commented 2 years ago

I would say have them implement both IDisposable and Async Disposable.

Also I would like it to where when each plugin gets loaded they are each loaded into their own AssemblyLoadContext which Remora.Plugins could track internally (and could be collectible so then Unload() can be called).

AraHaan commented 2 years ago

Also this could remove code like this in my bot:

image

And possibly simplify this:

image

to just:

AraHaan commented 2 years ago

hmm after thinking about this further I think the best option would be for an fake "service collection" that could be used for plugins to where their services can be added in a scoped way but still being able to access things from the original service provider (with the fake "service collection" able to build a fake "service provider" that will be able to obtain services from both the fake and the real one by first looking if the service is in the fake, and if it's not there it checks the real one.

Foxtrek64 commented 2 years ago

I wonder if we could implement this via our own concrete service provider, one that has the concept of "owned services". When registering a service, the consumer would do it exactly the same as the normally would, but those would effectively go into a scoped service provider specifically for that plugin. When requesting items from the service provider, all sources are searched so it appears to only be a single collection from the consumer.

This allows us to unload a plugin's services by simply disposing the scoped provider of the service we're taking down (as well as all dependent plugins, optional or not).

If we're doing that, I think it may also be good to implement named services, perhaps in the format of PluginId:ServiceName. This allows for multiple plugins to register things like a memory cache without worrying about collisions (though if we make a separate service provider, even faked, for each plugin, that could be solved simply by getting the plugin's own instance first and searching other stores separately).

There may be some consideration into private/public types too for each service registration. A plugin dealing with sensitive information, like an in-game currency, shouldn't be susceptible to tampering just because the name of the service that manages that currency is well known. This could possibly be accomplished via a separate provider for private or public registrations or a property on the registration itself, depending on how we want to scope things.

I'll work on the provider service in a separate library since I think it's a bit out of scope for this plugins library, and it's generic enough where others may find it useful.

Foxtrek64 commented 2 years ago

Service Provider idea here: https://github.com/LuzFaltex/LuzFaltex.Extensions.DependencyInjection

AraHaan commented 2 years ago

I have implemented a working service provider but it means registering a custom PluginServiceProviderFactory which then return an static instance that then lists the real service providers internally into a dictionary separated by plugin file name (without extension).

Edit: It works, but does not resolve anything properly that are not a part first service provider added inside of the PluginServiceProvider.

AraHaan commented 2 years ago

Turns out there is only a single place we could change to make an mutable service provider (I think) and we could technically copy the code from dotnet/runtime for it.

AraHaan commented 2 years ago

Also @Foxtrek64 on realoadable plugins that contain singleton services, should those singleton services be removed from it's cache inside of the custom ServiceProvider and then when when plugins are reloaded it is readded and a new instance of it be created?

Or should singleton service instances be avoided by plugins entirely?

AraHaan commented 2 years ago

Service Provider idea here: https://github.com/LuzFaltex/LuzFaltex.Extensions.DependencyInjection

And implemented: image

AraHaan commented 2 years ago

It fully works, the only thing to wait for now is 2022.48 on Remora.Discord and it should then work for Discord Bots.

https://github.com/Foxtrek64/Remora.Plugins/pull/1