Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I communicate between plugins?

I have a plugin system where I use MarshalByRefObject to create isolated domains per plugin, so users can reload their new versions, as they see fit without having to turn off the main application.

Now I have the need to allow a plugin to view which plugins are currently running and perhaps start/stop a specific plugin.

I know how to issue commands from the wrapper, in the below code for example:

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);
        }
    }
}

So then I could, for example:

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

Of course the above is just a resumed sample...

Then on my wrapper I have methods like:

bool Start(string name);
bool Stop(string name);

Which basically is a wrapper to issue the Start/Stop of a specific plugin from the list and a list to keep track of running plugins:

List<Plugin> Plugins

Plugin is just a simple class that holds Domain, RemoteLoader information, etc.

What I don't understand is, how to achieve the below, from inside a plugin. Be able to:

  • View the list of running plugins
  • Execute the Start or Stop for a specific plugin

Or if this is even possible with MarshalByRefObject given the plugins are isolated or I would have to open a different communication route to achieve this?

For the bounty I am looking for a working verifiable example of the above described...

like image 994
Guapo Avatar asked May 29 '16 11:05

Guapo


2 Answers

First let's define couple of interfaces:

// 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; }
}

Now a bit rewritten RemoteLoader (did not work for me as it was):

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;
    }
}

And a host:

// 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();
            }
        }
    }
}

Now let's create two different assemblies. Each of them needs only to reference interfaces IPlugin and IHostController. In first assembly define plugin:

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");
    }
}

In second assembly define another plugin:

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;
    }
}

Now in your main .exe which hosts all plugins:

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();
}

And the output is:

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
like image 159
Evk Avatar answered Nov 18 '22 01:11

Evk


You can make a plugin ask it's host to perform these actions. You can pass to the RemoteLoader an instance of a MarshalByRefObject derived class that is created by the host. The RemoteLoader can then use that instance to perform any action.

You also can make the plugins communicate with each other by passing a suitable MarshalByRefObject from the host to each plugin. I'd recommend routing all actions through the host, though, because it's a simpler architecture.

like image 3
usr Avatar answered Nov 18 '22 02:11

usr