Open Foxtrek64 opened 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).
Also this could remove code like this in my bot:
And possibly simplify this:
to just:
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.
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.
Service Provider idea here: https://github.com/LuzFaltex/LuzFaltex.Extensions.DependencyInjection
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
.
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.
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?
Service Provider idea here: https://github.com/LuzFaltex/LuzFaltex.Extensions.DependencyInjection
And implemented:
It fully works, the only thing to wait for now is 2022.48 on Remora.Discord and it should then work for Discord Bots.
After some discussions, we have outlined the following priorities for a plugins rewrite:
IPluginDescriptor
will provideStart()
andStop()
async methods, which accept a cancellation token.Start()
will return aTask<Result>
. A successful result indicates that tree initialization can continue. A failed result will abort initializing any child plugins which have a Required relationship.Stop()
will return void. A plugin must always be prepared to stop at any time.A plugin should also implement
IDisposable
to help clean up dependencies. Plugin developers may also implementIAsyncDisposable
if applicable to them.Plugin lifetime:
Please offer thoughts and ideas. This will be shaped into a proper proposal over time.