How can I communicate between plugins? - c#

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...

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

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.

Related

Validation Exception Queue using MSMQ in C#

I'm new in Microsoft Message Queue in Windows Server, I need to push, if the EmployeeID is NULL.
The Employee Model Class is
public class Employee
{
public string EmployeeID { get; set; }
public string EmployeeName { get; set; }
}
public void ValidationProcess(Employee emp)
{
if((emp != null) || (emp.EmployeeID == null))
{
// Push into Validation Exception Queue using MSMQ
}
}
Once the Data pushed into that Validation Exception Queue, it should be processed by separate process. Every 1hr the process need to initiate and it should call the following method
public void ValidationExceptionProcess(object obj)
{
// Some Inner Process
// Log the Error
}
Kindly guide me how to create and process it.
First Step:
Install MSMQs as a windows feature on the server/pc
Then:
- Create the queue if it does not exist
- Push the message in the queue asynchronously
Useful guide
Code example for pushing and retrieving messages from msmq:
public class ExceptionMSMQ
{
private static readonly string description = "Example description";
private static readonly string path = #".\Private$\myqueue";
private static MessageQueue exceptionQueue;
public static MessageQueue ExceptionQueue
{
get
{
if (exceptionQueue == null)
{
try
{
if (MessageQueue.Exists(path))
{
exceptionQueue = new MessageQueue(path);
exceptionQueue.Label = description;
}
else
{
MessageQueue.Create(path);
exceptionQueue = new MessageQueue(path);
exceptionQueue.Label = description;
}
}
catch
{
throw;
}
finally
{
exceptionQueue.Dispose();
}
}
return exceptionQueue;
}
}
public static void PushMessage(string message)
{
ExceptionQueue.Send(message);
}
private static List<string> RetrieveMessages()
{
List<string> messages = new List<string>();
using (ExceptionQueue)
{
System.Messaging.Message[] queueMessages = ExceptionQueue.GetAllMessages();
foreach (System.Messaging.Message message in queueMessages)
{
message.Formatter = new XmlMessageFormatter(
new String[] { "System.String, mscorlib" });
string msg = message.Body.ToString();
messages.Add(msg);
}
}
return messages;
}
public static void Main(string[] args)
{
ExceptionMSMQ.PushMessage("my exception string");
}
}
An other widely used way to do that would also be to use out of the box loggers which already contains this functionality like Enterprise Library or NLog which provide easy interfaces to do that.
For retrieving messages I would recommend a separate windows service which would periodically read messages and process them. An good example on how to do that is given here: Windows service with timer
Update: Windows Service Example:
MSMQConsumerService.cs
public partial class MSMQConsumerService : ServiceBase
{
private System.Timers.Timer timer;
public MSMQConsumerService()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
this.timer = new System.Timers.Timer(30000D); // 30000 milliseconds = 30 seconds
this.timer.AutoReset = true;
this.timer.Elapsed += new System.Timers.ElapsedEventHandler(this.ProcessQueueMessages);
this.timer.Start();
}
protected override void OnStop()
{
this.timer.Stop();
this.timer = null;
}
private void ProcessQueueMessages(object sender, System.Timers.ElapsedEventArgs e)
{
MessageProcessor.StartProcessing();
}
}
and the MessageProcessor.cs
public class MessageProcessor
{
public static void StartProcessing()
{
List<string> messages = ExceptionMSMQ.RetrieveMessages();
foreach(string message in messages)
{
//write message in database
}
}
}

Memory leaks when using MarshalByRef in .Net

I've come across an interesting issue, which is really two-fold I guess. I'll try and keep this focused though. I have an environment set up in which an assembly is programmatically compiled and loaded into a child app domain. A class from that child app domain's assembly is instantiated (it's actually marshaled back to the parent domain and a proxy is used there), and methods are executed against it.
The following resides in a satellite assembly:
namespace ScriptingSandbox
{
public interface ISandbox
{
object Invoke(string method, object[] parameters);
void Disconnect();
}
public class SandboxLoader : MarshalByRefObject, IDisposable
{
#region Properties
public bool IsDisposed { get; private set; }
public bool IsDisposing { get; private set; }
#endregion
#region Finalization/Dispose Methods
~SandboxLoader()
{
DoDispose();
}
public void Dispose()
{
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose()
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
Disconnect();
IsDisposed = true;
IsDisposing = false;
}
#endregion
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
// We don't want this to ever expire.
// We will disconnect it when we're done.
return null;
}
public void Disconnect()
{
// Close all the remoting channels so that this can be garbage
// collected later and we don't leak memory.
RemotingServices.Disconnect(this);
}
public ISandbox Create(string assemblyFileName, string typeName, object[] arguments)
{
// Using CreateInstanceFromAndUnwrap and then casting to the interface so that types in the
// child AppDomain won't be loaded into the parent AppDomain.
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
object instance = AppDomain.CurrentDomain.CreateInstanceFromAndUnwrap(assemblyFileName, typeName, true, bindingFlags, null, arguments, null, null);
ISandbox sandbox = instance as ISandbox;
return sandbox;
}
}
}
The class that is unwrapped from the child app domain is expected to implement the interface above. The SandboxLoader in the code above also runs in the child app domain, and serves the role of creating the target class. This is all tied in by the ScriptingHost class below, which runs in the parent domain in the main assembly.
namespace ScriptingDemo
{
internal class ScriptingHost : IDisposable
{
#region Declarations
private AppDomain _childAppDomain;
private string _workingDirectory;
#endregion
#region Properties
public bool IsDisposed { get; private set; }
public bool IsDisposing { get; private set; }
public string WorkingDirectory
{
get
{
if (string.IsNullOrEmpty(_workingDirectory))
{
_workingDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin");
}
return _workingDirectory;
}
}
#endregion
public ScriptingHost() { }
#region Finalization/Dispose Methods
~ScriptingHost()
{
DoDispose(false);
}
public void Dispose()
{
DoDispose(true);
GC.SuppressFinalize(this);
}
private void DoDispose(bool isFromDispose)
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
if (isFromDispose)
{
UnloadChildAppDomain();
}
IsDisposed = true;
IsDisposing = false;
}
private void UnloadChildAppDomain()
{
if (_childAppDomain == null) return;
try
{
bool isFinalizing = _childAppDomain.IsFinalizingForUnload();
if (!isFinalizing)
{
AppDomain.Unload(_childAppDomain);
}
}
catch { }
_childAppDomain = null;
}
#endregion
#region Compile
public List<string> Compile()
{
CreateDirectory(WorkingDirectory);
CreateChildAppDomain(WorkingDirectory);
CompilerParameters compilerParameters = GetCompilerParameters(WorkingDirectory);
using (VBCodeProvider codeProvider = new VBCodeProvider())
{
string sourceFile = GetSourceFilePath();
CompilerResults compilerResults = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFile);
List<string> compilerErrors = GetCompilerErrors(compilerResults);
return compilerErrors;
}
}
private string GetSourceFilePath()
{
DirectoryInfo dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
// This points a test VB.net file in the solution.
string sourceFile = Path.Combine(dir.Parent.Parent.FullName, #"Classes\Scripting", "ScriptingDemo.vb");
return sourceFile;
}
private void CreateDirectory(string path)
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
Directory.CreateDirectory(path);
}
private void CreateChildAppDomain(string workingDirectory)
{
AppDomainSetup appDomainSetup = new AppDomainSetup()
{
ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
PrivateBinPath = "bin",
LoaderOptimization = LoaderOptimization.MultiDomainHost,
ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust
};
Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
_childAppDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), evidence, appDomainSetup);
_childAppDomain.InitializeLifetimeService();
}
private CompilerParameters GetCompilerParameters(string workingDirectory)
{
CompilerParameters compilerParameters = new CompilerParameters()
{
GenerateExecutable = false,
GenerateInMemory = false,
IncludeDebugInformation = true,
OutputAssembly = Path.Combine(workingDirectory, "GeneratedAssembly.dll")
};
// Add GAC/System Assemblies
compilerParameters.ReferencedAssemblies.Add("System.dll");
compilerParameters.ReferencedAssemblies.Add("System.Xml.dll");
compilerParameters.ReferencedAssemblies.Add("System.Data.dll");
compilerParameters.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll");
// Add Custom Assemblies
compilerParameters.ReferencedAssemblies.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScriptingSandbox.dll"));
compilerParameters.ReferencedAssemblies.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScriptingInterfaces.dll"));
return compilerParameters;
}
private List<string> GetCompilerErrors(CompilerResults compilerResults)
{
List<string> errors = new List<string>();
if (compilerResults == null) return errors;
if (compilerResults.Errors == null) return errors;
if (compilerResults.Errors.Count == 0) return errors;
foreach (CompilerError error in compilerResults.Errors)
{
string errorText = string.Format("[{0}, {1}] :: {2}", error.Line, error.Column, error.ErrorText);
errors.Add(errorText);
}
return errors;
}
#endregion
#region Execute
public object Execute(string method, object[] parameters)
{
using (SandboxLoader sandboxLoader = CreateSandboxLoader())
{
ISandbox sandbox = CreateSandbox(sandboxLoader);
try
{
object result = sandbox.Invoke(method, parameters);
return result;
}
finally
{
if (sandbox != null)
{
sandbox.Disconnect();
sandbox = null;
}
}
}
}
private SandboxLoader CreateSandboxLoader()
{
object sandboxLoader = _childAppDomain.CreateInstanceAndUnwrap("ScriptingSandbox", "ScriptingSandbox.SandboxLoader", true, BindingFlags.CreateInstance, null, null, null, null);
return sandboxLoader as SandboxLoader;
}
private ISandbox CreateSandbox(SandboxLoader sandboxLoader)
{
string assemblyPath = Path.Combine(WorkingDirectory, "GeneratedAssembly.dll");
ISandbox sandbox = sandboxLoader.Create(assemblyPath, "ScriptingDemoSource.SandboxClass", null);
return sandbox;
}
#endregion
}
}
For reference, the ScriptingDemo.vb file that gets compiled:
Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.Globalization
Imports Microsoft.VisualBasic
Imports System.Data
Imports System.Text
Imports System.Text.RegularExpressions
Imports System.Xml
Imports System.Net
Imports System.ComponentModel
Imports System.Reflection
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Lifetime
Imports System.Security.Permissions
Imports ScriptingSandbox
Imports ScriptingInterfaces
Namespace ScriptingDemoSource
Public Class SandboxClass
Inherits MarshalByRefObject
Implements ISandbox
Public Sub Disconnect() Implements ISandbox.Disconnect
RemotingServices.Disconnect(Me)
End Sub
Public Function Invoke(ByVal methodName As String, methodParameters As Object()) As Object Implements ScriptingSandbox.ISandbox.Invoke
'Return Nothing
Dim type As System.Type = Me.GetType()
Dim returnValue As Object = type.InvokeMember(methodName, Reflection.BindingFlags.InvokeMethod + Reflection.BindingFlags.Default, Nothing, Me, methodParameters)
type = Nothing
Return returnValue
End Function
<SecurityPermissionAttribute(SecurityAction.Demand, Flags:=SecurityPermissionFlag.Infrastructure)> _
Public Overrides Function InitializeLifetimeService() As Object
Return Nothing
End Function
Function ExecuteWithNoParameters() As Object
Return Nothing
End Function
Function ExecuteWithSimpleParameters(a As Integer, b As Integer) As Object
Return a + b
End Function
Function ExecuteWithComplexParameters(o As ScriptingInterfaces.IMyInterface) As Object
Return o.Execute()
End Function
End Class
End Namespace
The first issue I ran into was that even after cleaning up the sandbox, memory leaked. This was resolved by keeping an instance of the sandbox around and not destroying it after executing methods from the script. This added/changed the following to the ScriptingHost class:
private ISandbox _sandbox;
private string _workingDirectory;
private void DoDispose(bool isFromDispose)
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
if (isFromDispose)
{
Cleanup();
}
IsDisposed = true;
IsDisposing = false;
}
private void CleanupSandboxLoader()
{
try
{
if (_sandboxLoader == null) return;
_sandboxLoader.Disconnect();
_sandboxLoader = null;
}
catch { }
}
private void CleanupSandbox()
{
try
{
if (_sandbox == null) return;
_sandbox.Disconnect();
}
catch { }
}
public void Cleanup()
{
CleanupSandbox();
CleanupSandboxLoader();
UnloadChildAppDomain();
}
public object Execute(string method, object[] parameters)
{
if (_sandboxLoader == null)
{
_sandboxLoader = CreateSandboxLoader();
}
if (_sandbox == null)
{
_sandbox = CreateSandbox(_sandboxLoader);
}
object result = _sandbox.Invoke(method, parameters);
return result;
}
This really didn't resolve the underlying issue (destroying the sandbox and loader didn't release memory as expected). As I have more control over that behavior, though, it did allow me to move on to the next issue.
The code that uses ScriptingHost looks like the following:
private void Execute()
{
try
{
List<MyClass> originals = CreateList();
for (int i = 0; i < 4000; i++)
{
List<MyClass> copies = MyClass.MembersClone(originals);
foreach (MyClass copy in copies)
{
object[] args = new object[] { copy };
try
{
object results = _scriptingHost.Execute("ExecuteWithComplexParameters", args);
}
catch (Exception ex)
{
_logManager.LogException("executing the script", ex);
}
finally
{
copy.Disconnect();
args.SetValue(null, 0);
args = null;
}
}
MyClass.ShallowCopy(copies, originals);
MyClass.Cleanup(copies);
copies = null;
}
MyClass.Cleanup(originals);
originals = null;
}
catch (Exception ex)
{
_logManager.LogException("executing the script", ex);
}
MessageBox.Show("done");
}
private List<MyClass> CreateList()
{
List<MyClass> myClasses = new List<MyClass>();
for (int i = 0; i < 300; i++)
{
MyClass myClass = new MyClass();
myClasses.Add(myClass);
}
return myClasses;
}
And the code for MyClass:
namespace ScriptingDemo
{
internal sealed class MyClass : MarshalByRefObject, IMyInterface, IDisposable
{
#region Properties
public int ID { get; set; }
public string Name { get; set; }
public bool IsDisposed { get; private set; }
public bool IsDisposing { get; private set; }
#endregion
public MyClass() { }
#region Finalization/Dispose Methods
~MyClass()
{
DoDispose();
}
public void Dispose()
{
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose()
{
if (IsDisposing) return;
if (IsDisposed) return;
IsDisposing = true;
Disconnect();
IsDisposed = true;
IsDisposing = false;
}
#endregion
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
// We don't want this to ever expire.
// We will disconnect it when we're done.
return null;
}
public void Disconnect()
{
// Close all the remoting channels so that this can be garbage
// collected later and we don't leak memory.
RemotingServices.Disconnect(this);
}
public object Execute()
{
return "Hello, World!";
}
public MyClass MembersClone()
{
MyClass copy = new MyClass();
copy.ShallowCopy(this);
return copy;
}
public void ShallowCopy(MyClass source)
{
if (source == null) return;
ID = source.ID;
Name = source.Name;
}
#region Static Members
public static void ShallowCopy(List<MyClass> sources, List<MyClass> targets)
{
if (sources == null) return;
if (targets == null) return;
int minCount = Math.Min(sources.Count, targets.Count);
for (int i = 0; i < minCount; i++)
{
MyClass source = sources[i];
MyClass target = targets[i];
target.ShallowCopy(source);
}
}
public static List<MyClass> MembersClone(List<MyClass> originals)
{
if (originals == null) return null;
List<MyClass> copies = new List<MyClass>();
foreach (MyClass original in originals)
{
MyClass copy = original.MembersClone();
copies.Add(copy);
}
return copies;
}
public static void Disconnect(List<MyClass> myClasses)
{
if (myClasses == null) return;
myClasses.ForEach(c => c.Disconnect());
}
public static void Cleanup(List<MyClass> myClasses)
{
if (myClasses == null) return;
myClasses.ForEach(c => c.Dispose());
myClasses.Clear();
myClasses.TrimExcess();
myClasses = null;
}
#endregion
}
}
As the code stands, memory slowly leaks the more iterations run and GCHandles soar through the roof. I've played with adding a finite lease instead of setting up the leases to never expire, but that caused wild fluctuations in memory that would eventually drop but not completely and ultimately still consumed more memory overall than the current solution (by a margin of dozens of megabytes).
I fully understand that creating a large number of classes like that and dropping them shortly there after is undesirable, but it simulates a much larger system. We may or may not address that issue, but for me I would like to better understand why the memory is leaking in the current system.
EDIT:
I just wanted to note that the memory leaking doesn't appear to be managed memory. Using various profiling tools, it appears that that the managed heaps tend to stay within a pretty set range whereas the unmanaged memory is what seems to grow.
EDIT #2
Rewriting the code to keep the list of classes around rather than dumping them every iteration does seem to alleviate the issues (my assumption is that this works because we're reusing everything we've already allocated), but I'd like to keep this open if only for an academic exercise. The root issue still is unresolved.

Attaching an event handler through reflexion

I have the following code:
namespace ConsoleApplication
{
static void Main(string[] args)
{
Device device = new Device();
device.Command += new EventHandler<DeviceSpecialArgs>(device_Command);
}
public static void device_Command(Object source, DeviceSpecialArgs args)
{
Console.WriteLine("Command: {0}, Reguest: {1}", args.Command, args.Request);
}
}
}
I have to do the exact same thing but the assembly containing types Device and DeviceSpecialArgs need to be loaded at runtime. I know how to load the assembly with reflection but I find the event handling part puzzling:
namespace ConsoleApplication
{
static void Main(string[] args)
{
// Load the assembly
string dllPath = #"C:\Temp\Device.dll"
Assembly asm = Assembly.LoadFrom(dllPath);
// Instanciate Device
Type deviceType = asm.GetType("Device");
object device = Activator.CreateInstance(deviceType);
// How do I subscribe to the Command event?
}
// args would normally be a DeviceSpecialArgs but since that type is
// unknown at compile time, how do I prototype the handler?
public static void device_Command(Object source, ??? args)
{
Console.WriteLine("Command: {0}, Reguest: {1}", args.Command, args.Request);
}
}
How do I subscribe to the event using reflection? Also, how should I prototype the handler itself since the type of "args" is unknown at compile time? FYI, I'm C# 3 and .NET 3.5.
Firsly, look at the MAF.
Alternative way is to add to the first assembly a reference to the second one. Then create a couple of interfaces in the second assembly and make the classes in the first one to implement them:
public interface IDeviceSpecialArgs
{
string Command { get; }
string Request { get; }
}
public interface IDevice
{
event EventHandler<IDeviceSpecialArgs> Command;
}
The first assembly:
public sealed class DeviceSpecialArgs : EventArgs, IDeviceSpecialArgs
{
private readonly string command;
private readonly string request;
public string Command
{
get { return command; }
}
public string Request
{
get { return request; }
}
public DeviceSpecialArgs(string command, string request)
{
this.command = command;
this.request = request;
}
}
public class Device : IDevice
{
public event EventHandler<IDeviceSpecialArgs> Command;
...
}
In the second assembly simply cast newly instantiated objects to the corresponding interfaces:
IDevice device = Activator.CreateInstance(deviceType) as IDevice;
Now you can subscribe to the Command event, because it is declared in the IDevice interface:
device.Command += new EventHandler<IDeviceSpecialArgs>(device_Command);
EDIT: If you have no control over the assembly you are loading, please try the following code. It just creates a handler with the EventArgs type of the second argument and uses a reflection to get its properties:
internal class DeviceEvent
{
private readonly Type deviceType;
private readonly Type deviceSpecialArgsType;
public DeviceEvent()
{
// Load the assembly
const string dllPath = #"C:\Temp\Device.dll";
Assembly asm = Assembly.LoadFrom(dllPath);
// Get types
deviceType = asm.GetType("Device");
deviceSpecialArgsType = asm.GetType("DeviceSpecialArgs");
// Instantiate Device
object device = Activator.CreateInstance(deviceType);
// Subscribe to the Command event
deviceType.GetEvent("Command").AddEventHandler(device, (Delegate.CreateDelegate(typeof(EventHandler), GetType().GetMethod("Device_Command", BindingFlags.NonPublic))));
}
private void Device_Command(object sender, EventArgs e)
{
string command = deviceSpecialArgsType.GetProperty("Command", BindingFlags.Public).GetValue(e, null).ToString();
string request = deviceSpecialArgsType.GetProperty("Request", BindingFlags.Public).GetValue(e, null).ToString();
...
}
}

C# Plugin using MarshalByRefObject creating multiple copies

I am putting together a plugin framework with these requirements:
load/unload plugins at will
call methods in loaded plugins
raise callback events from plugin to the owner
To do this I am creating a new AppDomain, and loading the Plugin assemblies into this.
The implementation I have so far is working to a degree, but I believe I am creating an instance of the plugin in the local appDomain and also the new AppDomain.
When I load first, I get duplicate callback messages. When I load/unload multiple times I get multiple callback messages being added to the list. This indicates to me that I am loading up the plugin assembly not only remotely but also locally, and thus my "unload" mechanism is not operating as I would like. I would be grateful if anyone can tell me where I am going wrong.
I also understand I need to take the "life time" of the plugin into account but not sure where to implement this.
Thanks.
(1) I have a plugin interface
public interface IPlugin
{
string Name();
string Version();
string RunProcess();
// custom event handler to be implemented, event arguments defined in child class
event EventHandler<PluginEventArgs> CallbackEvent;
//event EventHandler<EventArgs> CallbackEvent;
void OnProcessStart(PluginEventArgs data);
void OnProcessEnd(PluginEventArgs data);
}
(2) custom event args
[Serializable]
public class PluginEventArgs : EventArgs
{
public string ResultMessage;
public string executingDomain;
public PluginEventArgs(string resultMessage = "")
{
// default empty values allows us to send back default event response
this.ResultMessage = resultMessage;
this.executingDomain = AppDomain.CurrentDomain.FriendlyName;
}
}
(3) example plugin class implementation
[Serializable]
public class Plugin_1 : IPlugin
{
System.Timers.Timer counter;
int TimerInterval;
string PluginName = "My plugin";
public string Name()
{
return "CMD";
}
public bool Start()
{
OnStart(new PluginEventArgs());
RunProcess();
return true;
}
// OnTimer event, process start raised, sleep to simulate doing some work, then process end raised
public void OnCounterElapsed(Object sender, EventArgs e)
{
OnProcessStart(new PluginEventArgs());
OnProcessEnd(new PluginEventArgs());
Stop();
}
public bool Stop()
{
// simulate waiting for process to finish whatever its doing....
if (counter != null)
{
counter.Stop();
OnStop(new PluginEventArgs());
}
return true;
}
public string RunProcess()
{
TimerInterval = 2000;
if (counter == null){
counter = new System.Timers.Timer(TimerInterval);
}
else {
counter.Stop();
counter.Interval = TimerInterval;
}
counter.Elapsed += OnCounterElapsed;
counter.Start();
return "";
}
public event EventHandler<PluginEventArgs> CallbackEvent;
void OnCallback(PluginEventArgs e)
{
if (CallbackEvent != null)
{
CallbackEvent(this, e);
}
}
public void OnProcessStart(PluginEventArgs Data)
{
OnCallback(new PluginEventArgs(Data.executingDomain + " - " + PluginName + " started"));
}
public void OnProcessEnd(PluginEventArgs Data)
{
OnCallback(new PluginEventArgs(Data.executingDomain + " - " + PluginName + " ended"));
}
(4) I have a plugin manager that loads/unloads
public bool LoadPlugin()
{
try
{
Domain_Command = AppDomain.CreateDomain("Second_domain");
command_loader = (ProxyLoader)Domain_Command.CreateInstanceAndUnwrap("PluginMgr", "PluginMgr.Method");
Plugins.AddPlugin(command_loader.LoadAndExecute("APluginName", Plugins.ProxyLoader_RaiseCallbackEvent), SomePluginType, false);
return true;
}
catch (Exception ex)
{
string message = ex.Message;
return false;
}
}
(5) my "ProxyLoader" to load the plugin into separate AppDomain
public class ProxyLoader : MarshalByRefObject
{
public AssemblyInstanceInfo LoadAndExecute(string assemblyName, EventHandler<PluginContract.PluginEventArgs> proxyLoader_RaiseCallbackEvent)
{
AssemblyInstanceInfo AInfo = new AssemblyInstanceInfo();
//nb: this AppDomain.CurrentDomain is in its own context / different from the caller app domain?
Assembly pluginAssembly = AppDomain.CurrentDomain.Load(assemblyName);
foreach (Type type in pluginAssembly.GetTypes())
{
if (type.GetInterface("IPlugin") != null)
{
object instance = Activator.CreateInstance(type, null, null);
AInfo.ObjectInstance = instance;
string s = ((PluginContract.IPlugin)instance).RunProcess(); // main procedure
AInfo.ASM = pluginAssembly;
((PluginContract.IPlugin)instance).CallbackEvent += proxyLoader_RaiseCallbackEvent;
((PluginContract.IPlugin)instance).Start();
instance = null;
}
}
return AInfo;
}
}
(6) I have a callback this plugs into
public event EventHandler<PluginContract.PluginEventArgs> Callback;
void OnCallback(PluginContract.PluginEventArgs e)
{
if (Callback != null)
{
Callback(this, e);
}
}
(7) called by (referenced in ProxyLoader when load the assembly)
public void ProxyLoader_RaiseCallbackEvent(object source, PluginContract.PluginEventArgs e)
{
OnCallback(new PluginContract.PluginEventArgs(str));
}

C# Communication between plugin and host application

I am writing plugin based application.
Host application:
namespace CSK
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
LoadPlugins();
}
public void LoadPlugins()
{
DirectoryInfo di = new DirectoryInfo("./Plugins");
foreach (FileInfo fi in di.GetFiles("*_Plugin.dll"))
{
Assembly pluginAssembly = Assembly.LoadFrom(fi.FullName);
foreach (Type pluginType in pluginAssembly.GetExportedTypes())
{
if (pluginType.GetInterface(typeof(MainInterface.PluginHostInterface).Name) != null)
{
MainInterface.PluginHostInterface TypeLoadedFromPlugin = (MainInterface.PluginHostInterface)Activator.CreateInstance(pluginType);
MainInterface.IMother mother = new ApiMethods(this);
TypeLoadedFromPlugin.Initialize(mother);
}
}
}
}
}
Interface:
namespace MainInterface
{
public interface PluginHostInterface
{
void Initialize(IMother mother);
}
public interface IMother
{
MenuItem addMenuItem(String header, String name);
MenuItem addSubMenuItem(MenuItem menu, String header, String name);
Boolean receiveMessage(String message, String from);
Boolean addContact(String name, String status, String proto, String avatar = "av");
}
}
Test plugin:
namespace Plugin_Test
{
public class MainClass : MainInterface.PluginHostInterface
{
private MainInterface.IMother CSK;
public void Initialize(MainInterface.IMother mainAppHandler)
{
CSK = mainAppHandler;
}
}
}
And now, i want to execute some methods in Plugin Test from my host application. Of course, there will be many plugins, and not every of them will contain specified methods. I was trying to use events but with no success. Any idea how to do it?
Class with Events:
public class EventProvidingClass {
public event EventHandler SomeEvent;
public void InvokeSomeEvent() {
if(SomeEvent != null) SomeEvent.Invoke(this, new EventArgs());
}
}
Your Plugin Interface:
public interface PluginHostInterface
{
void Initialize(IMother mother);
void InitializeEvents(EventProvidingClass eventProvider);
}
Plugin Class:
public class MainClass : MainInterface.PluginHostInterface
{
private MainInterface.IMother CSK;
public void Initialize(MainInterface.IMother mainAppHandler)
{
CSK = mainAppHandler;
}
public void InitializeEvents(EventProvidingClass eventProvider)
{
eventProvider.SomeEvent += someEventHandler;
}
private void someEventHandler(object sender, EventArgs e)
{
}
}
and then call InitializeEvents after the Initialize function.
Of course you can put the events where you want them, you just need to make sure that you make them available for the Plugin so that the Plugin can assign its EventHandlers

Categories

Resources