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

Implement isolated plugins, runtime plugin load/unload #751

Open
wants to merge 10 commits into
base: development
Choose a base branch
from

Conversation

jhett12321
Copy link
Contributor

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.

Copy link

github-actions bot commented Feb 14, 2024

Test Results

       1 files     135 suites   1m 20s ⏱️
1 407 tests 1 407 ✔️ 0 💤 0
2 138 runs  2 138 ✔️ 0 💤 0

Results for commit 26962bf.

♻️ This comment has been updated with latest results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant