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.
Related
I was creating a small logger. For that purpose I have a class (LogFile) which publishes the Log(ILogCsvLine csvLine) method. This method adds the line to log to a queue (linesToLog) and sets a trigger, that has been registered to the ThreadPool with Logging(..) as method that should be executed on a different thread whenever there is a trigger and pending processor time. The Logging(..) method is writing the lines to log to a given file.
Now I ran into the problem, that the Dispose() method has been called, while the queue wasn't empty, resulting in a call to the Logging(..) method, while trigger or fileAccessLock were already disposed. As a solution I've build some checks around those EventWaitHandles and was wondering whether there is a better readable and more elegant way of doing this.
internal sealed class LogFile : ILogFile
{
private readonly EventWaitHandle fileAccessLock = new AutoResetEvent(true);
private readonly IFilesLoader filesLoader;
private readonly Queue<ILogCsvLine> linesToLog = new Queue<ILogCsvLine>();
private readonly IFile logFile;
private readonly object myLock = new object();
private RegisteredWaitHandle registeredWait;
private readonly EventWaitHandle trigger = new AutoResetEvent(false);
private IDirectory directory = null;
private bool disposeFileAccessLock = false;
private bool disposeTrigger = false;
private bool isDisposed = false;
private bool isDisposing = false;
private bool isLogging = false;
private bool isSettingTrigger = false;
public event EventHandler<FilesException> LoggingFailed;
public LogFile(IFile logFile, IFilesLoader filesLoader)
{
this.filesLoader = filesLoader;
this.logFile = logFile;
Setup();
}
private void Setup()
{
directory = logFile.ParentDirectory;
EnforceFileExists();
registeredWait = ThreadPool.RegisterWaitForSingleObject(trigger, Logging, null, -1, false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~LogFile()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
if (!isDisposed)
{
try
{
lock (myLock)
isDisposing = true;
registeredWait?.Unregister(trigger);
if (disposing)
{
if (isSettingTrigger)
disposeTrigger = true;
else
trigger?.Dispose();
if (isLogging)
disposeFileAccessLock = true;
else
fileAccessLock?.Dispose();
}
}
finally
{
isDisposed = true;
lock (myLock)
isDisposing = false;
}
}
}
public IFile File => logFile;
public void Log(ILogCsvLine csvLine)
{
lock (myLock)
{
if (isDisposing || isDisposed)
return;
linesToLog.Enqueue(csvLine);
isSettingTrigger = true;
}
trigger.Set();
lock (myLock)
{
isSettingTrigger = false;
if (disposeTrigger)
trigger?.Dispose();
}
}
private void Logging(object data, bool timedOut)
{
ILogCsvLine line = null;
lock (myLock)
{
if (linesToLog.Count == 0)
return;
if (isDisposing || isDisposed)
return;
line = linesToLog.Dequeue();
isLogging = true;
}
fileAccessLock.WaitOne();
FilesException occurredException = null;
IStreamWriter sw = null;
try
{
EnforceFileExists();
sw = logFile.AppendText();
do
{
sw.WriteLine(line.ToCsvString());
lock (myLock)
{
if (linesToLog.Count > 0)
line = linesToLog.Dequeue();
else
line = null;
}
} while (line != null);
}
catch (Exception e)
{
if (e is ThreadAbortException)
throw;
string message = string.Format("Error writing to {0}. {1}", logFile.Path, e.Message);
occurredException = new FilesException(message, e);
}
finally
{
if (sw != null)
{
sw.Flush();
sw.Close();
sw = null;
}
}
fileSizeManager?.Check();
fileAccessLock.Set();
lock (myLock)
{
if (disposeFileAccessLock)
fileAccessLock?.Dispose();
isLogging = false;
}
if (occurredException != null)
LoggingFailed?.Invoke(this, occurredException);
}
private void EnforceFileExists()
{
if (!directory.Exists)
directory.Create();
if (!logFile.Exists)
{
var fileAccess = filesLoader.GetFileAccess();
fileAccess.Create(logFile.Path, FileSystemRights.Read | FileSystemRights.Write);
}
}
}
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.
I have a question about disposing objects.
Consider this IDisposable class
public class MyClass : DisposableParentClass
{
private MyProp _prop;
public MyClass(MyProp prop)
{
_prop = prop;
}
public MyClass()
{
_prop = new MyProp();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_prop.Dispose();
}
base.Dispose(disposing);
}
}
On the first constructor, MyProp is injected. So MyClass is not the owner of the object. But on the second constructor, MyProp is created locally.
Should I always dispose MyProp, or should I check first if it is injected or not.
public class MyClass : DisposableParentClass
{
private MyProp _prop;
private bool _myPropInjected = false;
public MyClass(MyProp prop)
{
_prop = prop;
_myPropInjected = true;
}
public MyClass()
{
_prop = new MyProp();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (!_myPropInjected) { _prop.Dispose(); }
}
base.Dispose(disposing);
}
}
If your class should handle these two situations:
It is not the owner of the provided object, it should not dispose of it
It is the owner of the created object, it should dispose of it
Then yes, you need to have a mechanism that tells these two situations apart.
A common method (common to me anyway) is to use naming convention like this:
private MyProp _prop;
private bool _ownsProp = false;
ie. reverse the meaning of your flags, but this is details, your solution is just fine, and yes, you need to have a solution like this.
If you have a ton of these fields, where each must have its own bool field to handle this, it might be worth creating a helper class, such as this LINQPad program demonstrates:
void Main()
{
Injectable i1 = new Injectable();
Injectable i2 = new Injectable(new Injected("A"));
Injectable i3 = new Injectable(new Injected("A"), new Injected("B"));
Debug.WriteLine("dispose a and b");
i1.Dispose();
Debug.WriteLine("dispose b");
i2.Dispose();
Debug.WriteLine("no dispose");
i3.Dispose();
}
public class Injected : IDisposable
{
public Injected(string name) { Name = name; }
public string Name { get; set; }
public void Dispose() { Debug.WriteLine(Name + " disposed"); }
}
public class Injectable : IDisposable
{
private Ownable<Injected> _A;
private Ownable<Injected> _B;
public Injectable(Injected a, Injected b)
{
_A = Ownable.NotOwned(a);
_B = Ownable.NotOwned(b);
}
public Injectable(Injected a)
{
_A = Ownable.NotOwned(a);
_B = Ownable.Owned(new Injected("B"));
}
public Injectable()
{
_A = Ownable.Owned(new Injected("A"));
_B = Ownable.Owned(new Injected("B"));
}
public void Dispose()
{
_A.Dispose();
_B.Dispose();
}
}
public class Ownable<T> : IDisposable
where T : class
{
private readonly T _Instance;
private readonly Action _CleanupAction;
public Ownable(T instance, bool isOwned)
{
_Instance = instance;
if (isOwned)
{
IDisposable disposable = instance as IDisposable;
if (disposable == null)
throw new NotSupportedException("Unable to clean up owned object, does not implement IDisposable");
_CleanupAction = () => disposable.Dispose();
}
}
public Ownable(T instance, Action cleanupAction)
{
_Instance = instance;
_CleanupAction = cleanupAction;
}
public T Instance { get { return _Instance; } }
public void Dispose()
{
if (_CleanupAction != null)
_CleanupAction();
}
}
public static class Ownable
{
public static Ownable<T> Owned<T>(T instance)
where T : class
{
return new Ownable<T>(instance, true);
}
public static Ownable<T> Owned<T>(T instance, Action cleanupAction)
where T : class
{
return new Ownable<T>(instance, cleanupAction);
}
public static Ownable<T> NotOwned<T>(T instance)
where T : class
{
return new Ownable<T>(instance, false);
}
}
A different note can be made here either.
It depends on what is your MyClass is doing actually.
For example, if we are talking about a class that reads video stream from device, after applies some filters to it and writes data to a user specified file, where file writing is made by stream passed from the outside, say like this:
public class VideoProcessor : IDisposable {
private FileStream _videoFile = null;
private VideoProcessor() {}
//user specified FileStream
public VideoProcessor(FileStream fs) {_videoFile = fs;}
public void Dispose() {
_videoFile.Dispose(); //Dispose user passed FileStream
}
}
disposing passed stream object during dispose call, makes actually sence.
In other cases, yes, it's better to not destroy object, if you are not an owner of it. Leave it to the caller to decide when it is appropriate time to do that.
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));
}
I write a C# class library and want to use it in vbscript.
Here is my code:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.PointOfService;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using System.Reflection;
using System.ComponentModel;
namespace IndigoDynamic
{
#region class implements IAsyncResult
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
public class AsyncResult : IAsyncResult
{
object _state;
private bool m_completed;
private System.Threading.ManualResetEvent m_handle;
private Exception m_exception;
public bool IsCompleted
{
get { return m_completed; }
set { m_completed = value; }
}
public System.Threading.WaitHandle AsyncWaitHandle
{
get { return m_handle; }
set { m_handle = (System.Threading.ManualResetEvent)value; }
}
public object AsyncState
{
get
{
if (Exception != null)
{
throw Exception;
}
return _state;
}
internal set
{
_state = value;
}
}
public bool CompletedSynchronously { get { return IsCompleted; } }
internal Exception Exception
{
get { return m_exception; }
set { m_exception = value; }
}
}
#endregion
#region extends CashDrawer
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
public class MyCashDrawer
{
private CashDrawer me;
public delegate void status_callback(int newstatus);
private status_callback myF;
public MyCashDrawer(CashDrawer param)
{
me = param;
me.StatusUpdateEvent += new StatusUpdateEventHandler(this.StatusUpdate);
}
[ComVisible(true)]
public void Claim(int timeout) { me.Claim(timeout); }
[ComVisible(true)]
public void Close() { me.Close(); }
[ComVisible(true)]
public void Open() { me.Open(); }
[ComVisible(true)]
public void OpenDrawer() { me.OpenDrawer(); }
[ComVisible(true)]
public void Release() { me.Release(); }
[ComVisible(true)]
public void Release(int timeout, int freq, int duration, int delay)
{
me.WaitForDrawerClose(timeout, freq, duration, delay);
}
[ComVisible(true)]
public int StatusClosed() { return CashDrawer.StatusClosed; }
[ComVisible(true)]
public int StatusOpen() { return CashDrawer.StatusOpen; }
[ComVisible(true)]
public bool Claimed() { return me.Claimed; }
[ComVisible(true)]
public bool DeviceEnabled() { return me.DeviceEnabled; }
[ComVisible(true)]
public bool DrawerOpened() { return me.DrawerOpened; }
[ComVisible(true)]
public ControlState State() { return me.State; }
[ComVisible(true)]
public void addStatusCallback(status_callback f)
{
myF = f;
}
[ComVisible(true)]
public void removeStatusCallback(status_callback f)
{
if (myF == f)
myF = null;
}
[ComVisible(true)]
private void StatusUpdate(object sender, StatusUpdateEventArgs arg)
{
if (myF != null)
myF(arg.Status);
}
}
#endregion
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
class VirtualManager : ISynchronizeInvoke
{
private readonly object _sync;
// Constructor
public VirtualManager()
{
_sync = new object();
}
#region implements methods of ISynchronizeInvoke
public IAsyncResult BeginInvoke(Delegate method, object[] args) {
AsyncResult result = new AsyncResult();
System.Threading.ThreadPool.QueueUserWorkItem(delegate {
result.AsyncWaitHandle = new System.Threading.ManualResetEvent(false);
try {
result.AsyncState = Invoke(method, args);
} catch (Exception exception) {
result.Exception = exception;
}
result.IsCompleted = true;
});
return result;
}
public object EndInvoke(IAsyncResult result) {
if (!result.IsCompleted) {
result.AsyncWaitHandle.WaitOne();
}
return result.AsyncState;
}
public object Invoke(Delegate method, object[] args) {
lock (_sync) {
return method.DynamicInvoke(args);
}
}
public bool InvokeRequired {
get { return true; }
}
#endregion
[ComVisible(true)]
public MyCashDrawer getCashDrawer()
{
PosExplorer posExplorer = new PosExplorer(this);
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return new MyCashDrawer(cd);
}
}
[ComVisible(true)]
public MyCashDrawer getCashDrawer(String name)
{
PosExplorer posExplorer = new PosExplorer(this);
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer, name);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return new MyCashDrawer(cd);
}
}
[ComRegisterFunction()]
public static void RegisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
RegistryKey ctrl = k.CreateSubKey("Control");
ctrl.Close();
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase);
inprocServer32.Close();
k.Close();
}
[ComUnregisterFunction()]
public static void UnregisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
if (k == null)
{
return;
}
k.DeleteSubKey("Control", false);
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.DeleteSubKey("CodeBase", false);
inprocServer32.Close();
k.Close();
}
}
}
After building, I use RegAsm but it threw Warning No types are registered.
Then I write a sample code in vbs but it says ActiveX can not create Object.
Sub main
set objTest = CreateObject("IndigoDynamic.VirtualManager")
end sub
call main
Somebody said that I have to check AssemblyInfo.cs and make sure I have
[assembly: ComVisible(true)]
Of course I have, but problem is still not solved.
Can anybody tell me a solution?
I change my code like that. More simple, no threading, no interface, all is public.
But it still doesn't work.
Please, I really need help.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.PointOfService;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using System.Reflection;
using System.ComponentModel;
namespace IndigoDynamic
{
[ProgId("IndigoDynamic.VirtualManager")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ComVisible(true)]
public class VirtualManager
{
public VirtualManager()
{
}
[ComVisible(true)]
public CashDrawer getCashDrawer()
{
PosExplorer posExplorer = new PosExplorer();
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return cd;
}
}
[ComVisible(true)]
public CashDrawer getCashDrawer(String name)
{
PosExplorer posExplorer = new PosExplorer();
DeviceInfo deviceInfo = posExplorer.GetDevice(DeviceType.CashDrawer, name);
if (deviceInfo == null)
{
//<report failure >
return null;
}
else
{
CashDrawer cd = posExplorer.CreateInstance(deviceInfo) as CashDrawer;
return cd;
}
}
[ComRegisterFunction()]
public static void RegisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
RegistryKey ctrl = k.CreateSubKey("Control");
ctrl.Close();
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase);
inprocServer32.Close();
k.Close();
}
[ComUnregisterFunction()]
public static void UnregisterClass(string key)
{
StringBuilder sb = new StringBuilder(key);
sb.Replace(#"HKEY_CLASSES_ROOT\", "");
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
if (k == null)
{
return;
}
k.DeleteSubKey("Control", false);
RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);
inprocServer32.DeleteSubKey("CodeBase", false);
inprocServer32.Close();
k.Close();
}
}
}
You are unfortunately very far removed from a workable solution. The warning is accurate, none of the classes you made [ComVisible] at creatable by a COM client. MyCashDrawer is missing the required default constructor, a COM client app cannot pass arguments to a constructor. VirtualManager isn't public and derives from an interface that's not [ComVisible]
The code is also missing the required implementations for interfaces that makes an ActiveX component work on a ActiveX host window, like IOleObject, IOleInPlaceObject, IOleInplaceActiveObject, IOleWindow, IViewObject, etcetera. Furthermore, you are exposing implementation details that an ActiveX object can't take care of, the threading model in COM is very different from the one in .NET.
You are going to need a seriously different approach. Consider deriving the visible object from System.Windows.Forms.Control, it takes care of the minimum ActiveX interface implementation requirements. And make threading your problem, don't leave it up to the client to sort it out.
The regasm warning isn't the problem.
Maybe it's beacause VirtualManager class isn't public. Try to expose your class as public.