I'm about to start a migration project at work for a legacy system written in VBScript. It has an interesting structure in that much of it was segregated by writing various components as "WSC" files, which are effectively a way of exposing VBScript code in a COM-like manner. The boundary interface from the "core" to these components is fairly tight and well known so I was hoping that I would be able to tackle writing a new core and reuse the WSCs, postponing their rewriting.
It's possible to load a WSC by adding a reference to "Microsoft.VisualBasic" and calling
var component = (dynamic)Microsoft.VisualBasic.Interaction.GetObject("script:" + controlFilename, null);
where "controlFilename" is the full file path. GetObject returns a reference of type "System.__ComObject" but the properties and methods can be accessed using .net's "dynamic" type.
This appeared to work fine initially, but I've encountered problems when quite a specific set of circumstances are combined - my worry is that this may happen in other cases or, even worse, that bad things are happening much of the time and being masked, just waiting to blow up when I least expect it.
The raised exception is of type "System.ExecutionEngineException", which sounds particularly scary (and vague)!
I've cobbled together what I believe to be the minimum reproduce case and was hoping that someone could cast a little light on what the problem could be. I've also identified some tweaks that can be made that seem to prevent it, though I can't explain why.
Create a new empty "ASP.NET Web Application" called "WSCErrorExample" (I've done this in VS 2013 / .net 4.5 and VS 2010 / .net 4.0, it makes no difference)
Add a reference to "Microsoft.VisualBasic" to the project
Add a new "Web Form" called "Default.aspx" and paste the following over the top of "Default.aspx.cs"
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.VisualBasic;
namespace WSCErrorExample
{
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
var currentFolder = GetCurrentDirectory();
var logFile = new FileInfo(Path.Combine(currentFolder, "Log.txt"));
Action<string> logger = message =>
{
// The try..catch is to avoid IO exceptions when reproducing by requesting the page many times
try { File.AppendAllText(logFile.FullName, message + Environment.NewLine); }
catch { }
};
var controlFilename = Path.Combine(currentFolder, "TestComponent.wsc");
var control = (dynamic)Interaction.GetObject("script:" + controlFilename, null);
logger("About to call Go");
control.Go(new DataProvider(logger));
logger("Completed");
}
private static string GetCurrentDirectory()
{
// This is a way to get the working path that works within ASP.Net web projects as well as Console apps
var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
if (path.StartsWith(#"file:\", StringComparison.InvariantCultureIgnoreCase))
path = path.Substring(6);
return path;
}
[ComVisible(true)]
public class DataProvider
{
private readonly Action<string> _logger;
public DataProvider(Action<string> logger)
{
_logger = logger;
}
public DataContainer GetDataContainer()
{
return new DataContainer();
}
public void Log(string content)
{
_logger(content);
}
}
[ComVisible(true)]
public class DataContainer
{
public object this[string fieldName]
{
get { return "Item:" + fieldName; }
}
}
}
}
Add a new "Text File" called "TestComponent.wsc", open its properties window and change "Copy to Output Directory" to "Copy if newer" then paste the following in as its content
<?xml version="1.0" ?>
<?component error="false" debug="false" ?>
<package>
<component id="TestComponent">
<registration progid="TestComponent" description="TestComponent" version="1" />
<public>
<method name="Go" />
</public>
<script language="VBScript">
<![CDATA[
Function Go(objDataProvider)
Dim objDataContainer: Set objDataContainer = objDataProvider.GetDataContainer()
If IsEmpty(objDataContainer) Then
mDataProvider.Log "No data provided"
End If
End Function
]]>
</script>
</component>
</package>
Running this once should cause no apparent issue, the "Log.txt" file will be written to the "bin" folder. Refreshing the page, however, normally results in an exception
Managed Debugging Assistant 'FatalExecutionEngineError' has detected a problem in 'C:\Program Files (x86)\IIS Express\iisexpress.exe'.
Additional information: The runtime has encountered a fatal error. The address of the error was at 0x733c3512, on thread 0x1e10. The error code is 0xc0000005. This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code. Common sources of this bug include user marshaling errors for COM-> interop or PInvoke, which may corrupt the stack.
Occasionally, the second request does not result in this exception, but holding down F5 in the browser window for a couple of seconds will ensure that it rears its ugly head. The exception, so far as I can tell, happens at the "If IsEmpty" check (other versions of this reproduce case had more logging calls, which pointed to that line being the source of the problem).
I've tried various things to try to get to the bottom of this, I've tried to recreate in a console app and the problem does not occur, even if I spin up hundreds of threads and get them to process the work above. I've tried an ASP.Net MVC web application, rather than using a Web Form and the same issue DOES occur. I've tried changing the apartment state from the default MTA to STA (I was clutching at straws a bit at that point!) and it made no change to the behavour. I've tried building a web project that uses Microsoft's OWIN implementation and the issue occurs in that scenario as well.
Two interesting things that I have noticed - if the "DataContainer" class does not have an indexed property (or a default method / property, decorated with a [DispId(0)] attribute - not illustrated in this example) then the error does not occur. If the "logger" closure does not contain a "FileInfo" reference (if a string "logFilePath" was maintained, rather than the FileInfo instance "logFile") then the error does not occur. I suppose it sounds like one approach would be to avoid doing these things! But I would be concerned that there could be other ways to trigger this scenario that I don't currently know about and trying to enforce the rule of not-doing-these-things could get complicated as the code base grows, I can imagine this error creeping back in without it being immediately obvious why.
On one run (through Katana), I got additional call stack information:
This thread is stopped with only external code frames on the call stack. External code frames are typically from framework code but can also include other optimized modules which are loaded in the target process.
Call stack with external code
mscorlib.dll!System.Variant.Variant(object obj)
mscorlib.dll!System.OleAutBinder.ChangeType(object value, System.Type type, System.Globalization.CultureInfo cultureInfo)
mscorlib.dll!System.RuntimeType.TryChangeType(object value, System.Reflection.Binder binder, System.Globalization.CultureInfo culture, bool needsSpecialCast)
mscorlib.dll!System.RuntimeType.CheckValue(object value, System.Reflection.Binder binder, System.Globalization.CultureInfo culture, System.Reflection.BindingFlags invokeAttr)
mscorlib.dll!System.Reflection.MethodBase.CheckArguments(object[] parameters, System.Reflection.Binder binder, System.Reflection.BindingFlags invokeAttr, System.Globalization.CultureInfo culture, System.Signature sig)
[Native to Managed Transition]
One final note: if I create a wrapper for the "DataProvider" class, using IReflect and map the calls over IDispatch on to calls to the underlying "DataProvider" instance then the issue goes away. But again, deciding that this is somehow the answer seems dangerous to me - if I have to be meticulous about ensuring that any reference passed to the components has such a wrapper then errors could creep in that could be difficult to track down. What if a reference that IS encased in an IReflect-implementing wrapper returns a reference from a method or property call that isn't wrapped up in the same way? I suppose the wrapper could try to do something like ensuring it only returns "safe" reference (ie. those without indexed properties or DispId=0 methods or properties) without wrapping them in a further IReflect wrapper.. but it all seems a bit hacky.
I really have no clue where next to go with this problem, does anyone have any idea?
My guess is, the error you're seeing is caused by the fact that WSC script components are COM STA objects by their nature. They're implemented by the underlying VBScript Active Scripting Engine, which itself is an STA COM object. As such, they require an STA thread to be created and accessed on, and such thread should remain the same for the lifetime of any particular WSC object (the object requires thread affinity).
ASP.NET threads are not STA. They're ThreadPool threads, and they implicitly become COM MTA threads when you start using COM objects on them (for differences between STA and MTA, refer to INFO: Descriptions and Workings of OLE Threading Models). COM then creates a separate implicit STA apartment for your WSC objects and marshal calls there from your ASP.NET request thread. The whole thing may or may not go well in the ASP.NET environment.
Ideally, you should get rid of WSC script components and replace them with .NET assemblies. If that's not feasible short-term, I'd recommend that you run your own explicitly controlled STA thread(s) to host the WSC components. The following may help:
How to use non-thread-safe async/await APIs and patterns with ASP.NET Web API?
StaTaskScheduler and STA thread message pumping
Updated, why not give this a try? Your code would look like this:
// create a global instance of ThreadAffinityTaskScheduler - per web app
public static class GlobalState
{
public static ThreadAffinityTaskScheduler TaScheduler { get; private set; }
public static GlobalState()
{
GlobalState.TaScheduler = new ThreadAffinityTaskScheduler(
numberOfThreads: 10,
staThreads: true,
waitHelper: WaitHelpers.WaitWithMessageLoop);
}
}
// ... inside Page_Load
GlobalState.TaScheduler.Run(() =>
{
var control = (dynamic)Interaction.GetObject("script:" + controlFilename, null);
logger("About to call Go");
control.Go(new DataProvider(logger));
logger("Completed");
}, CancellationToken.None).Wait();
If that works, you can somewhat improve the web app's scalabilty by using PageAsyncTask and async/await instead of the blocking Wait().
Related
I have a Coded UI test method:
public void MyTestMethod()
{
string baseUrl = "www.google.com";
GlobalVariable.browser = BrowserWindow.Launch(new System.Uri(baseUrl));
GlobalVariable.browser.NavigateToUrl(new System.Uri(baseUrl + "/images"));
string expected = baseUrl + "/images";
Assert.AreEqual(expected, GlobalVariable.browser.Uri);
}
However, the value of GlobalVariable.browser.Uri at the time of the assertion is still pointing to www.google.com, even though the browser was successfully navigated to the expected. I've tried setting a Playback.Wait() to ensure that I'm not asserting too early. Strangely enough, this only happens on one or two develoment environments (the others show the correct value for GlobalVariable.browser.Uri), leading me to believe that there's some environmental variable rather than a code issue.
Also, if, instead of statically setting and updating the GlobalVariable.browser object, we call a get function each time we call the object (like so:
private BrowserWindow _browser;
public BrowserWindow browser
{
get
{
BrowserWindow currentWindow = BrowserWindow.FromProcess(_browser.Process);
return currentWindow;
}
set
{
_browser = value;
return _browser;
}
}
), then the object is created based on the system process and has the correct properties. So essentially, the BrowserWindow object created during our initialization method isn't getting updated as it goes along, and we have to create a new object based on the process. Again, this only occurs on some remote environments and not on the dev machines set up locally. What am I missing?
Underneath it all the Microsoft.VisualStudio.TestTools.UITesting.IEBrowserService which provides both the NavigateToUrl and the Uri get methods delegate their calls to an internal class called InternetExplorerWrapper which is a COM wrapper to a Window Handle. The code internally checks repetedly on a method UpdateWebBrowserReferenceIfInvalid() and recreates the IEBrowserService instance when needeed. Because of this repeated checking, I assume that even the test framework cannot guarantee that the instance of IE that it is dealing with doens't "go away" and need re-connecting with. It depends on the lifetime of the window handle its created out of I guess.
In conclusion, the underlying code repeatedly recreates the IEBrowserService which provides your Uri getter, and it does this in a non-deterministic manner, so by repeating this pattern (creating the browser window on demand) you are just repeating a pattern that the microsoft guys themselves have used internally.
I've just "earned" the privilege to maintain a legacy library coded in C# at my current work.
This dll:
Exposes methods for a big legacy system made with Uniface, that has no choice but calling COM objects.
Serves as a link between this legacy system, and another system's API.
Uses WinForm for its UI in some cases.
More visually, as I understand the components :
*[Big legacy system in Uniface]* ==[COM]==> [C# Library] ==[Managed API]==> *[Big EDM Management System]*
The question is: One of the methods in this C# Library takes too long to run and I "should" make it asynchronous!
I'm used to C#, but not to COM at all. I've already done concurrent programming, but COM seems to add a lot of complexity to it and all my trials so far end in either:
A crash with no error message at all
My Dll only partially working (displaying only part of its UI, and then closing), and still not giving me any error at all
I'm out of ideas and resources about how to handle threads within a COM dll, and I would appreciate any hint or help.
So far, the biggest part of the code I've changed to make my method asynchronous :
// my public method called by the external system
public int ComparedSearch(string application, out string errMsg) {
errMsg = "";
try {
Action<string> asyncOp = AsyncComparedSearch;
asyncOp.BeginInvoke(application, null, null);
} catch (ex) {
// ...
}
return 0;
}
private int AsyncComparedSearch(string application) {
// my actual method doing the work, that was the called method before
}
Any hint or useful resource would be appreciated.
Thank you.
UPDATE 1:
Following answers and clues below (especially about the SynchronizationContext, and with the help of this example) I was able to refactor my code and making it to work, but only when called from another Window application in C#, and not through COM.
The legacy system encounters a quite obscure error when I call the function and doesn't give any details about the crash.
UPDATE 2:
Latest updates in my trials: I managed to make the multithreading work when the calls are made from a test project, and not from the Uniface system.
After multiple trials, we tend to think that our legacy system doesn't support well multithreading in its current config. But that's not the point of the question any more :)
Here is a exerpt of the code that seems to work:
string application;
SynchronizationContext context;
// my public method called by the external system
public int ComparedSearch(string application, out string errMsg) {
this.application = application;
context = WindowsFormsSynchronizationContext.Current;
Thread t = new Thread(new ThreadStart(AsyncComparedSearchAndShowDocs));
t.Start();
errMsg = "";
return 0;
}
private void AsyncComparedSearch() {
// ANY WORK THAT AS NOTHING TO DO WITH UI
context.Send(new SendOrPostCallback(
delegate(object state)
{
// METHODS THAT MANAGE UI SOMEHOW
}
), null);
}
We are now considering other solutions than modifying this COM assembly, like encapsulating this library in a Windows Service and creating an interface between the system and the service. It should be more sustainable..
It is hard to tell without knowing more details, but there are few issues here.
You execute the delegate on another thread via BeginInvoke but you don't wait for it. Your try\catch block won't catch anything as it has already passed while the remote call is still being executed. Instead, you should put try\catch block inside AsyncComparedSearch.
As you don't wait for the end of the execution of remote method (EndInvoke or via callback) I am not sure how do you handle the results of the COM call. I guess then that you update the GUI from within AsyncComparedSearch. If so, it is wrong, as it is running on another thread and you should never update GUI from anywhere but the GUI thread - it will most likely result with a crash or other unexpected behavior. Therefore, you need to sync the GUI update work to GUI thread. In WinForms you need to use Control.BeginInvoke (don't confuse it with Delegate.BeginInvoke) or some other way (e.g. SynchronizationContext) to sync the code to GUI thread. I use something similar to this:
private delegate void ExecuteActionHandler(Action action);
public static void ExecuteOnUiThread(this Form form, Action action)
{
if (form.InvokeRequired) { // we are not on UI thread
// Invoke or BeginInvoke, depending on what you need
form.Invoke(new ExecuteActionHandler(ExecuteOnUiThread), action);
}
else { // we are on UI thread so just execute the action
action();
}
}
then I call it like this from any thread:
theForm.ExecuteOnUiThread( () => theForm.SomeMethodWhichUpdatesControls() );
Besides, read this answer for some caveats.
I have researched this a fair bit and cannot establish the correct approach. My problem is as follows: I have a winForms applications and from within it I wish to launch a time intesive .dll. I can do this using System.Reflection no problem like this
// Execute the method from the requested .dll using reflection (System.Reflection).
//[System.Runtime.InteropServices.DllImport(strDllPath)]
DLL = Assembly.LoadFrom(strDllPath);
classType = DLL.GetType(String.Format("{0}.{0}", ListUfCmdParams[1]));
classInst = Activator.CreateInstance(classType);
XmlExpInfo = classType.GetMethod(DllParams[0]);
XmlExpInfo.Invoke(classInst, paramObj);
// Return something.
return String.Format("Method '{0}' from '{1}{2}' successfully executed!",
ListUfCmdParams[2], ListUfCmdParams[1], strDotDll);
this works great but the process being called is so time intensive I want to display to the user what is happening. To do this I have included in the .dll file a WinForm which has a progressBar and some other attributes. When I do this I get an exception. This occurs when "Activator.CreateInstance()" attempts to do its work: MissingMethodException "Cannot create an abstract class". I have come across this error before when I using partial classes and I had to remove the "partial" keyword from my classes to enable the .dll to execute correctly (which I just about got away with!). I cannot remove this "partial" keyword from the above winForms class so, the question is "How do I call a winForm from within my .dll (if indeed it is possible)?" so that the .dll can show its progress as it executes from the calling application?
Thanks for your time,
Nick
Ps. I have read the following threads and they are somewhat ambiguous:
A DLL with WinForms that can be launched from A main app
et al.
You should not make a callee (the dll) aware of it's caller (the form). Instead you could enrich the class in your dll that performs the time intensive method with a ProgressUpdated event:
public event ProgressUpdatedHandler ProgressUpdated;
public delegate void ProgressUpdatedHandler(object sender, int stepsCompleted, int stepsTotal)
This way the form could simply assign a handler for that event, and the dll could raise the event whenever it can indicate what the progress is.
I have just seen this question again and thought I would update as to how I eventually did this.
In the end I found the following to be the most effective way of performing the above for what I wanted. First you launch a WinForm which holds your progress information. Second youu envoke your "worker" method from within the "Shown" event.
The code for the first part i.e. to call the WinForm using Reflection is provided below:
// Execute the method from the requested .dll using reflection (System.Reflection).
Assembly DLL = Assembly.LoadFrom(strDllPath);
Type classType = DLL.GetType(String.Format("{0}.{0}", strNsCn));
object classInst = Activator.CreateInstance(classType, paramObj);
Form dllWinForm = (Form)classInst;
dllWinForm.ShowDialog();
I hope this helps someone else.
Background
I have a Windows service that uses various third-party DLLs to perform work on PDF files. These operations can use quite a bit of system resources, and occasionally seem to suffer from memory leaks when errors occur. The DLLs are managed wrappers around other unmanaged DLLs.
Current Solution
I'm already mitigating this issue in one case by wrapping a call to one of the DLLs in a dedicated console app and calling that app via Process.Start(). If the operation fails and there are memory leaks or unreleased file handles, it doesn't really matter. The process will end and the OS will recover the handles.
I'd like to apply this same logic to the other places in my app that use these DLLs. However, I'm not terribly excited about adding more console projects to my solution, and writing even more boiler-plate code that calls Process.Start() and parses the output of the console apps.
New Solution
An elegant alternative to dedicated console apps and Process.Start() seems to be the use of AppDomains, like this: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx
I've implemented similar code in my application, but the unit tests have not been promising. I create a FileStream to a test file in a separate AppDomain, but don't dispose it. I then attempt to create another FileStream in the main domain, and it fails due to the unreleased file lock.
Interestingly, adding an empty DomainUnload event to the worker domain makes the unit test pass. Regardless, I'm concerned that maybe creating "worker" AppDomains won't solve my problem.
Thoughts?
The Code
/// <summary>
/// Executes a method in a separate AppDomain. This should serve as a simple replacement
/// of running code in a separate process via a console app.
/// </summary>
public T RunInAppDomain<T>( Func<T> func )
{
AppDomain domain = AppDomain.CreateDomain ( "Delegate Executor " + func.GetHashCode (), null,
new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory } );
domain.DomainUnload += ( sender, e ) =>
{
// this empty event handler fixes the unit test, but I don't know why
};
try
{
domain.DoCallBack ( new AppDomainDelegateWrapper ( domain, func ).Invoke );
return (T)domain.GetData ( "result" );
}
finally
{
AppDomain.Unload ( domain );
}
}
public void RunInAppDomain( Action func )
{
RunInAppDomain ( () => { func (); return 0; } );
}
/// <summary>
/// Provides a serializable wrapper around a delegate.
/// </summary>
[Serializable]
private class AppDomainDelegateWrapper : MarshalByRefObject
{
private readonly AppDomain _domain;
private readonly Delegate _delegate;
public AppDomainDelegateWrapper( AppDomain domain, Delegate func )
{
_domain = domain;
_delegate = func;
}
public void Invoke()
{
_domain.SetData ( "result", _delegate.DynamicInvoke () );
}
}
The unit test
[Test]
public void RunInAppDomainCleanupCheck()
{
const string path = #"../../Output/appdomain-hanging-file.txt";
using( var file = File.CreateText ( path ) )
{
file.WriteLine( "test" );
}
// verify that file handles that aren't closed in an AppDomain-wrapped call are cleaned up after the call returns
Portal.ProcessService.RunInAppDomain ( () =>
{
// open a test file, but don't release it. The handle should be released when the AppDomain is unloaded
new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None );
} );
// sleeping for a while doesn't make a difference
//Thread.Sleep ( 10000 );
// creating a new FileStream will fail if the DomainUnload event is not bound
using( var file = new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ) )
{
}
}
Application domains and cross-domain interaction is a very thin matter, so one should make sure he really understands how thing work before doing anything... Mmm... Let's say, "non-standard" :-)
First of all, your stream-creating method actually executes on your "default" domain (surprise-surprise!). Why? Simple: the method that you pass into AppDomain.DoCallBack is defined on an AppDomainDelegateWrapper object, and that object exists on your default domain, so that is where its method gets executed. MSDN doesn't say about this little "feature", but it's easy enough to check: just set a breakpoint in AppDomainDelegateWrapper.Invoke.
So, basically, you have to make do without a "wrapper" object. Use static method for DoCallBack's argument.
But how do you pass your "func" argument into the other domain so that your static method can pick it up and execute?
The most evident way is to use AppDomain.SetData, or you can roll your own, but regardless of how exactly you do it, there is another problem: if "func" is a non-static method, then the object that it's defined on must be somehow passed into the other appdomain. It may be passed either by value (whereas it gets copied, field by field) or by reference (creating a cross-domain object reference with all the beauty of Remoting). To do former, the class has to be marked with a [Serializable] attribute. To do latter, it has to inherit from MarshalByRefObject. If the class is neither, an exception will be thrown upon attempt to pass the object to the other domain. Keep in mind, though, that passing by reference pretty much kills the whole idea, because your method will still be called on the same domain that the object exists on - that is, the default one.
Concluding the above paragraph, you are left with two options: either pass a method defined on a class marked with a [Serializable] attribute (and keep in mind that the object will be copied), or pass a static method. I suspect that, for your purposes, you will need the former.
And just in case it has escaped your attention, I would like to point out that your second overload of RunInAppDomain (the one that takes Action) passes a method defined on a class that isn't marked [Serializable]. Don't see any class there? You don't have to: with anonymous delegates containing bound variables, the compiler will create one for you. And it just so happens that the compiler doesn't bother to mark that autogenerated class [Serializable]. Unfortunate, but this is life :-)
Having said all that (a lot of words, isn't it? :-), and assuming your vow not to pass any non-static and non-[Serializable] methods, here are your new RunInAppDomain methods:
/// <summary>
/// Executes a method in a separate AppDomain. This should serve as a simple replacement
/// of running code in a separate process via a console app.
/// </summary>
public static T RunInAppDomain<T>(Func<T> func)
{
AppDomain domain = AppDomain.CreateDomain("Delegate Executor " + func.GetHashCode(), null,
new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory });
try
{
domain.SetData("toInvoke", func);
domain.DoCallBack(() =>
{
var f = AppDomain.CurrentDomain.GetData("toInvoke") as Func<T>;
AppDomain.CurrentDomain.SetData("result", f());
});
return (T)domain.GetData("result");
}
finally
{
AppDomain.Unload(domain);
}
}
[Serializable]
private class ActionDelegateWrapper
{
public Action Func;
public int Invoke()
{
Func();
return 0;
}
}
public static void RunInAppDomain(Action func)
{
RunInAppDomain<int>( new ActionDelegateWrapper { Func = func }.Invoke );
}
If you're still with me, I appreciate :-)
Now, after spending so much time on fixing that mechanism, I am going to tell you that is was purposeless anyway.
The thing is, AppDomains won't help you for your purposes. They only take care of managed objects, while unmanaged code can leak and crash all it wants. Unmanaged code doesn't even know there are such things as appdomains. It only knows about processes.
So, in the end, your best option remains your current solution: just spawn another process and be happy about it. And, I would agree with the previous answers, you don't have to write another console app for each case. Just pass a fully qualified name of a static method, and have the console app load your assembly, load your type, and invoke the method. You can actually package it pretty neatly in a very much the same way as you tried with AppDomains. You can create a method called something like "RunInAnotherProcess", which will examine the argument, get the full type name and method name out of it (while making sure the method is static) and spawn the console app, which will do the rest.
You don't have to create many console applications, you can create a single application that will receive as parameter the full qualified type name. The application will load that type and execute it.
Separating everything into tiny processes is the best method to really dispose all the resources. An application domain cannot do full resources disposing, but a process can.
Have you considered opening a pipe between the main application and the sub applications? This way you could pass more structured information between the two applications without parsing standard output.
I have a a C# (FFx 3.5) application that loads DLLs as plug-ins. These plug-ins are loaded in separate AppDomains (for lots of good reasons, and this architecture cannot change). This is all well and good.
I now have a requirement to show a Dialog from one of those plug-ins. Bear in mind that I cannot return the dialog Form to the main application and have it displayed there (the current infrastructure doesn't support it).
Failure 1
In my DLL I created a Form and called Show. The dialog outline showed up but did not paint and it doesn't respond to mouse events. I assumed that this is becasue the DLL is in a separate AppDomain and the message pump for the app is somehow unable to dispatch messages to the new Form.
Failure 2
In my DLL I created a Form and called ShowDialog, which by all rights should create an internal message pump for the dialog.. The dialog is displayed and responded to clicks (hooray), but it appears that the primary app no longer is processing or dispatching windows messages because it quits painting and no longer responds to mouse events. For some reason now it seems that the main app's message pump is not dispatching.
Failure 3
In my DLL I created a Form and called Application.Run. This will certainly create a complete second message pump. I get the same behavior as Failure 2 - the Dialog behaves, but the calling app does not.
Any thoughts on what exactly is going on here and how I might go about showing a dialog from the other AppDomain's DLL and have both the caller and the callee still respond and paint properly?
Try using appdomain1's main form's BeginInvoke with a delegate that displays the form from appdomain2. So in Pseudocode:
Appdomain1:
AppDomain2.DoSomething(myMainForm);
AppDomain2:
DoSomething(Form parent)
{
Form foolishForm = new Form();
parent.BeginInvoke(new Action( delegate { foolishForm.Show(); } ));
}
The code may not be perfect, but it demonstrates the concept.
By the way, if you are having problems passing forms around because of remoting you can:
public class Container<T> : MarshalByRefObject
{
private T _value;
public T Value { get { return _value; } set { _value = value; } }
public Container() { }
public Container(T value) { Value = value; }
public static implicit operator T(Container<T> container)
{
return container.Value;
}
}
That will contain object you throw at it.
We have a very similarly architected application that loads DLL files and plugins. Each DLL file is loaded in a separate application domain, which is created on a separate thread. We have a third-party control in a form that would not appear unless we call System.Windows.Forms.Application.DoEvents() regularly.
Pseudo code:
<In new thread>
<Application domain created. Start called inside new application domain.>
<Start loads new DLL file, calls init function in DLL file>
<Start loops, calling DoEvents until the DLL file exits>
<Application domain unloaded>
<Thread exits>
This solved all of our GUI issues.
One thing that I've used before is implementing a DomainManager. It's possible to customize the various application domain security/binding/context's to handle complex or chicken-egg type problems with respect to pumping your data where you want ;)
I've ususally done this from a native.exe, bootstrapping the CLR through the COM interfaces (psudo code but the order and method names are correct ;):
CorBindToRuntimeEx()
SetHostControl()
GetCLRControl()
SetAppDomainManagerType("yourdomainmanger","info")
// Domain manager set before starting runtime
Start()
HostControl -- GetDomainManagerForDefaultDomain()
DomainManager -- Run()
Your domain manager can be any CLR class library, so their's not that much more native C.
A side note, if you were in WPF; I really like using the "Microsoft.DwayneNeed.Controls" method. Where you may have disperate threads with their own Dispatcher pump in the same UI control (not needing to resort to entirely new Window()'s).
The unique thing about using this approach, is that even if the primary UI thread is blocked/busy (some heavy operation, scanning the filesystem, etc...), these other threads may paint/update their UIElement's without any hiccup.