nwn-dotnet / Anvil

A Neverwinter Nights Server API
https://nwn-dotnet.github.io/Anvil
MIT License
24 stars 9 forks source link

Implement isolated plugins, runtime plugin load/unload #751

Open jhett12321 opened 9 months ago

jhett12321 commented 9 months ago

Background

The core design of Anvil merges all [ServiceBinding] types from plugins and Anvil itself into a single container, which is then wired up by LightInject to correctly resolve all dependencies:

Anvil -------|                              |- Service 1
Plugin1 -----|--------- Container ----------|- Service 2
Plugin2 -----|                              |- Service 3

This works really well for simple configurations, and for easily setting up service dependencies between plugins and anvil. E.g. if I want to use the WindowManager from the toolbox plugin, I can simply reference the plugin assembly and declare the dependency in my service class:

  [ServiceBinding(typeof(TestService))]
  public sealed class TestService
  {
    [Inject]
    private WindowManager WindowManager { get; init; }

    public void ShowWindow(NwPlayer player)
    {
      WindowManager.OpenWindow<MyWindowView, MyWindowController>(player);
    }
  }

One drawback to this approach is that all plugins + Anvil must be loaded and unloaded as one unit, making it impossible to toggle or configure them individually.

Isolated plugins

This PR introduces the concept of isolated plugins. An isolated plugin is configured by defining the PluginInfoAttribute on the plugin assembly:

[assembly: Anvil.Plugins.PluginInfo(Isolated = true)]

When a plugin is configured as isolated, it is given a dedicated container that cannot be used by other plugins or anvil when resolving dependencies:

Anvil -------|                              |- Service 1
Plugin1 -----|--------- Container ----------|- Service 2
Plugin2 -----|              ^               |- Service 3
                            |
Isolated Plugin |-----Plugin Container -----|- Service 4

The plugin container can use services from other plugins and anvil, but its own services cannot be used by other plugins.

This allows isolated plugins to be loaded/unloaded without disrupting anvil or other plugins that may be referencing it.

PluginManager API

The following methods have been exposed in the PluginManager to support runtime loading/unloading of plugins:

    /// <summary>
    /// Loads an isolated anvil plugin from the specified plugin folder at runtime.
    /// </summary>
    /// <param name="pluginRoot">The root folder containing the plugin assembly, and other resources.</param>
    /// <returns>The loaded plugin.</returns>
    /// <exception cref="ArgumentException">Thrown if the plugin folder/assembly is missing, or otherwise cannot be loaded.</exception>
    /// <exception cref="InvalidOperationException">Thrown if the plugin is already loaded, or if the specified plugin is not configured as an isolated plugin.</exception>
    public Plugin LoadPlugin(string pluginRoot);

    /// <summary>
    /// Unloads an isolated anvil plugin at runtime.
    /// </summary>
    /// <param name="plugin">The plugin to unload - see <see cref="GetPlugin(string)"/>.</param>
    /// <param name="waitForUnload">If true, the server will block the current main thread until the plugin has been unloaded.</param>
    /// <returns>A weak reference to the unloading plugin assembly. Query the <see cref="WeakReference.IsAlive"/> property to confirm the plugin has unloaded.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the plugin is not loaded, or if the specified plugin is not configured as an isolated plugin.</exception>
    public WeakReference UnloadPlugin(Plugin plugin, bool waitForUnload = true)

Disabled/Skipped plugins

Plugins may be disabled/prevented from loading by specifying ANVIL_PLUGINNAME_SKIP=true, replacing "PLUGINNAME" with the plugin that should not be loaded.

Isolated plugins disabled this way may still be loaded from the PluginManager APIs.

github-actions[bot] commented 9 months ago

Test Results

       1 files     135 suites   1m 20s :stopwatch: 1 407 tests 1 407 :heavy_check_mark: 0 :zzz: 0 :x: 2 138 runs  2 138 :heavy_check_mark: 0 :zzz: 0 :x:

Results for commit 26962bfe.

:recycle: This comment has been updated with latest results.