如何在插件之间进行通信?

6
我有一个插件系统,其中我使用MarshalByRefObject创建每个插件的隔离域,因此用户可以重新加载他们认为合适的新版本,而无需关闭主应用程序。
现在我需要允许一个插件查看当前正在运行的插件,并可能启动/停止特定的插件。
我知道如何从包装器中发出命令,在下面的代码中:
using System;
using System.Linq;
using System.Reflection;
using System.Security.Permissions;

namespace Wrapper
{
    public class RemoteLoader : MarshalByRefObject
    {
        private Assembly _pluginAassembly;
        private object _instance;
        private string _name;

        public RemoteLoader(string assemblyName)
        {
            _name = assemblyName;
            if (_pluginAassembly == null)
            {
                _pluginAassembly = AppDomain.CurrentDomain.Load(assemblyName);
            }

            // Required to identify the types when obfuscated
            Type[] types;
            try
            {
                types = _pluginAassembly.GetTypes();
            }
            catch (ReflectionTypeLoadException e)
            {
                types = e.Types.Where(t => t != null).ToArray();
            }

            var type = types.FirstOrDefault(type => type.GetInterface("IPlugin") != null);
            if (type != null && _instance == null)
            {
                _instance = Activator.CreateInstance(type, null, null);
            }
        }
    
        public void Start()
        {
            if (_instance == null)
            {
                return;
            }
            ((IPlugin)_instance).OnStart();
        }

        public void Stop()
        {
            if (_instance == null)
            {
                return;
            }
            ((IPlugin)_instance).OnStop(close);
        }
    }
}

那么我可以,例如:

var domain = AppDomain.CreateDomain(Name, null, AppSetup);
var assemblyPath = Assembly.GetExecutingAssembly().Location;
var loader = (RemoteLoader)Domain.CreateInstanceFromAndUnwrap(assemblyPath, typeof(RemoteLoader).FullName);
loader.Start();

当然,上述内容只是一个简要示例...
然后在我的封装器中,我有像这样的方法:
bool Start(string name);
bool Stop(string name);

基本上它是一个包装器,用于从列表中发出特定插件的启动/停止指令和一个用于跟踪正在运行的插件的列表:
List<Plugin> Plugins

插件只是一个简单的类,包含DomainRemoteLoader信息等。

我不明白的是,如何从插件内部实现以下操作:

  • 查看正在运行的插件列表
  • 执行特定插件的启动或停止操作

如果插件被隔离了,那么使用MarshalByRefObject是否可能实现此功能?还是说我需要打开另一条通信路线来实现这个操作?

对于悬赏,我希望能够得到上述描述的可工作可验证的示例...

2个回答

4

首先,让我们定义一些接口:

// this is your host
public interface IHostController {
    // names of all loaded plugins
    string[] Plugins { get; }
    void StartPlugin(string name);
    void StopPlugin(string name);
}
public interface IPlugin {
    // with this method you will pass plugin a reference to host
    void Init(IHostController host);
    void Start();
    void Stop();                
}
// helper class to combine app domain and loader together
public class PluginInfo {
    public AppDomain Domain { get; set; }
    public RemoteLoader Loader { get; set; }
}

现在稍微改写了RemoteLoader(因为原来的版本对我来说不起作用):

public class RemoteLoader : MarshalByRefObject {
    private Assembly _pluginAassembly;
    private IPlugin _instance;
    private string _name;

    public void Init(IHostController host, string assemblyPath) {
        // note that you pass reference to controller here
        _name = Path.GetFileNameWithoutExtension(assemblyPath);
        if (_pluginAassembly == null) {
            _pluginAassembly = AppDomain.CurrentDomain.Load(File.ReadAllBytes(assemblyPath));
        }

        // Required to identify the types when obfuscated
        Type[] types;
        try {
            types = _pluginAassembly.GetTypes();
        }
        catch (ReflectionTypeLoadException e) {
            types = e.Types.Where(t => t != null).ToArray();
        }

        var type = types.FirstOrDefault(t => t.GetInterface("IPlugin") != null);
        if (type != null && _instance == null) {
            _instance = (IPlugin) Activator.CreateInstance(type, null, null);
            // propagate reference to controller futher
            _instance.Init(host);
        }
    }

    public string Name => _name;
    public bool IsStarted { get; private set; }

    public void Start() {
        if (_instance == null) {
            return;
        }
        _instance.Start();
        IsStarted = true;
    }

    public void Stop() {
        if (_instance == null) {
            return;
        }
        _instance.Stop();
        IsStarted = false;
    }
}

并且有一个主机:

// note : inherits from MarshalByRefObject and implements interface
public class HostController : MarshalByRefObject, IHostController {        
    private readonly Dictionary<string, PluginInfo> _plugins = new Dictionary<string, PluginInfo>();

    public void ScanAssemblies(params string[] paths) {
        foreach (var path in paths) {
            var setup = new AppDomainSetup();                
            var domain = AppDomain.CreateDomain(Path.GetFileNameWithoutExtension(path), null, setup);
            var assemblyPath = Assembly.GetExecutingAssembly().Location;
            var loader = (RemoteLoader) domain.CreateInstanceFromAndUnwrap(assemblyPath, typeof (RemoteLoader).FullName);
            // you are passing "this" (which is IHostController) to your plugin here
            loader.Init(this, path);                          
            _plugins.Add(loader.Name, new PluginInfo {
                Domain = domain,
                Loader = loader
            });
        }
    }

    public string[] Plugins => _plugins.Keys.ToArray();

    public void StartPlugin(string name) {
        if (_plugins.ContainsKey(name)) {
            var p = _plugins[name].Loader;
            if (!p.IsStarted) {
                p.Start();
            }
        }
    }

    public void StopPlugin(string name) {
        if (_plugins.ContainsKey(name)) {
            var p = _plugins[name].Loader;
            if (p.IsStarted) {
                p.Stop();
            }
        }
    }
}

现在让我们创建两个不同的程序集。它们只需要引用接口IPlugin和IHostController。在第一个程序集中定义插件:
public class FirstPlugin : IPlugin {
    const string Name = "First Plugin";

    public void Init(IHostController host) {
        Console.WriteLine(Name + " initialized");
    }

    public void Start() {
        Console.WriteLine(Name + " started");
    }

    public void Stop() {
        Console.WriteLine(Name + " stopped");
    }
}

在第二个汇编中定义另一个插件:
public class FirstPlugin : IPlugin {
    const string Name = "Second Plugin";
    private Timer _timer;
    private IHostController _host;

    public void Init(IHostController host) {
        Console.WriteLine(Name + " initialized");
        _host = host;
    }

    public void Start() {
        Console.WriteLine(Name + " started");
        Console.WriteLine("Will try to restart first plugin every 5 seconds");
        _timer = new Timer(RestartFirst, null, 5000, 5000);
    }

    int _iteration = 0;
    private void RestartFirst(object state) {
        // here we talk with a host and request list of all plugins
        foreach (var plugin in _host.Plugins) {
            Console.WriteLine("Found plugin " + plugin);
        }
        if (_iteration%2 == 0) {
            Console.WriteLine("Trying to start first plugin");
            // start another plugin from inside this one
            _host.StartPlugin("Plugin1");
        }
        else {
            Console.WriteLine("Trying to stop first plugin");
            // stop another plugin from inside this one
            _host.StopPlugin("Plugin1");
        }
        _iteration++;
    }

    public void Stop() {
        Console.WriteLine(Name + " stopped");
        _timer?.Dispose();
        _timer = null;
    }
}

现在,你的主要 .exe 文件中托管了所有插件:

static void Main(string[] args) {
    var host = new HostController();
    host.ScanAssemblies(@"path to your first Plugin1.dll", @"path to your second Plugin2.dll");                  
    host.StartPlugin("Plugin2");
    Console.ReadKey();
}

输出结果为:

First Plugin initialized
Second Plugin initialized
Second Plugin started
Will try to restart first plugin every 5 seconds
Found plugin Plugin1
Found plugin Plugin2
Trying to start first plugin
First Plugin started
Found plugin Plugin1
Found plugin Plugin1
Found plugin Plugin2
Trying to stop first plugin
Found plugin Plugin2
Trying to stop first plugin
First Plugin stopped
First Plugin stopped
Found plugin Plugin1
Found plugin Plugin2
Trying to stop first plugin

我觉得自己很傻,这很简单,但是我无法从usr的话中理解如何实现:( 谢谢Evk让这个概念变得非常容易理解和实现。 - Guapo
伙计,Evk你在抢走赏金。我愿意用更少的积分来完成它! - usr

3
你可以让插件请求它的主机执行这些操作。你可以向 RemoteLoader 传递一个由主机创建的 MarshalByRefObject 派生类的实例。然后,RemoteLoader 可以使用该实例来执行任何操作。
你还可以通过将适当的 MarshalByRefObject 从主机传递给每个插件,使插件之间相互通信。虽然这是一种更简单的架构,但我建议将所有操作都通过主机进行路由。

嗨,谢谢你的回答,你是指像在方法中使用一个输出参数来处理列表吗?或者你能提供一些假设性的例子吗? - Guapo
在主机域中创建以下内容并将其传递给插件域:class HostController : MBRO { public void TalkToOtherPlugin(...); } - usr
这正是我卡住的部分,不明白如何进一步操作。你的 class HostController : MBRO { public void TalkToOtherPlugin(...); } 和我的 class RemoteLoader : MBRO { public void Start(); } 有什么不同呢?别误会,我只是不明白如何让被加载到隔离域中的插件调用那个方法或其他任何东西。 - Guapo
完全没有区别。使用RemoteLoader,您可以将插件域的句柄传递给主机。使用HostController,您可以将主机域的句柄传递给插件。这有意义吗?您可以将HostController传递给RemoteLoader。 - usr
我理解逻辑,但不知道如何应用它。我需要将我的包装器dll引用到插件中吗?因为目前插件不需要引用任何东西,只需要一个仅包含接口的公共dll。最初我认为可以将控制器作为RemoteLoader的out参数传递,但我不确定那是否可行。 - Guapo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接