all groups > dotnet clr > october 2004 >
You're in the

dotnet clr

group:

Plugin difficulties


Plugin difficulties vacindak NO[at]SPAM gmail.com
10/27/2004 2:47:58 PM
dotnet clr: Right now, I'm trying to implement a flexible method of allowing users
to add the capability to read and process new data formats through a
plugin framework. I have an abstract base class, "SourceObject",
which has a method "Create" which takes a string key that represents
the type of data being processed, IE, "xml" should load the
XmlTableSourceObject subclass, "csv" should load the CsvSourceObject
subclass and so on. The base class then has a hashtable mapping
string keys to Type objects for the various subclasses.

The problem is, the application itself is running as a windows service
and I don't want to have to make the user shut down the service and
restart it to install a new source object implementation if I don't
have to. I wanted to use the shadow copy functionality, load the
subclasses in a second AppDomain, use a FileSystemWatcher to track
changes to the plugin directory, and dynamically unload and reload the
second AppDomain when changes occur.

My purpose in using the second AppDomain is simply to group the
classes that might get loaded and unloaded and to reduce the number of
types I have to search through to find subclasses and register them
with the abstract base class. (I'm planning on looping through all
classes in the second AppDomain, checking if they're subclasses of the
base class in the primary AppDomain, and registering their type and
key with the base class if so.)

Also, I have a group of subclasses that I ideally want installed by
default and to exist at compile time because they're going to be used
quite a bit more often than the user-added ones and performance is
going to be an issue here. Realistically, I can register those types
with the base class manually, I just don't want to force the end user
to have to do that themselves with the plugin classes.

I think my questions are: A) Does anyone see any problems with my
plan? B) I've heard that sometimes the FileSystemWatcher gets
overzealous and fires too many times, is there anything I can do to
ensure that it only fires once for a set of changes, preferably
launching the unload/reload routine a set period of time after the
FileSystemWatcher has fired, possibly extending the timer if the
FileSystemWatcher spots more changes prior to the unload/reload
routine getting called. I'm hoping this will make allowance for
multiple dlls being dropped in the plugin directory at once (which
normally, I assume, would cause the unload/reload method to be called
way more times than it should have). C) What happens if the
unload/reload routine gets called but references to objects on the
now-unloaded secondary AppDomain still exist on the primary one? I
assume those are really just proxies and still exist but would throw
Re: Plugin difficulties vacindak NO[at]SPAM gmail.com
10/30/2004 8:57:48 PM
I'm currently having the exact same problem as was described here:

http://blogs.msdn.com/suzcook/archive/2003/06/12/57169.aspx#57179

I discovered that Eric Gunnerson's article actually makes a fairly
nasty mistake. He has a copy local reference to the plugin dll in the
main application. This is a little confusing because at first it
almost appears as if he's loading it from the plugin directory but
examination of the fusion logs demonstrates that he's actually loading
from the erroneous locally copied dll.

The problem is, if you set the ApplicationBase to the plugin directory
and then set the PrivateBinPath to the bin directory, fusion drops the
following warning:

WRN: Not probing location
file:///D:/rapidCANVAS/rapidDEVELOP/bin/rapidCOMMON.DLL, because the
location falls outside of the appbase.
WRN: Not probing location
file:///D:/rapidCANVAS/rapidDEVELOP/bin/rapidCOMMON/rapidCOMMON.DLL,
because the location falls outside of the appbase.
WRN: Not probing location
file:///D:/rapidCANVAS/rapidDEVELOP/bin/rapidCOMMON.EXE, because the
location falls outside of the appbase.
WRN: Not probing location
file:///D:/rapidCANVAS/rapidDEVELOP/bin/rapidCOMMON/rapidCOMMON.EXE,
because the location falls outside of the appbase.

And since that's exactly where it should be looking, the call to
CreateInstanceAndUnwrap fails with a FileNotFoundException.

If you try to be clever and set the ApplicationBase to the bin
directory and the PrivateBinPath to the plugin subdirectory, instead,
you get a strange SerializationException saying that there is
insufficient state to deserialize the object.

I managed to get it working by copying the plugins to the bin folder,
loading them, and then immediately deleting them (they're shadow
copied). But if anyone does anything silly like name the dll the same
as any of the files in the bin directory, the program will attempt to
overwrite (it'll probably fail here) and then delete itself. So I'm
not really keen on using that as a solution.

(Eric Gunnerson's article:
Re: Plugin difficulties vacindak NO[at]SPAM gmail.com
11/1/2004 4:53:51 AM
Would it help if I were to put the common library that contained the
RemoteLoader class in the GAC? Fusion checks the ApplicationBase,
Re: Plugin difficulties vacindak NO[at]SPAM gmail.com
11/18/2004 12:41:58 PM
[quoted text, click to view]

As it turns out, the solution to this particular problem was that
PrivateBinPath MUST be a relative path. Unfortunately, the
documentation is very abiguous in this regard. (It says that the
PrivateBinPath is combined with the ApplicationBase to probe for
assemblies. It's not immediately obvious whether "combine" means
concatenation, or appending it to the end of the list of places to
probe. I made the mistake of assuming the latter.) If you set an
absolute path, the fusion logs indicate perfectly normal looking
values, and you receive the entirely unhelpful SerializationException.
It took me weeks to find an obscure post on a forum mentioning that
it had to be a relative path, and this could quite easily have been
avoided if the docs had simply said "relative path" instead of what it
currently says. Anyone at Microsoft want to fix that please? Judging
by the forum post I found, I was not the only person to have that
misunderstanding.

Of course, now I am having a new problem. I've gotten the impression
that I cannot, in the primary AppDomain, use references to Types or
Assemblies that are loaded in the secondary AppDomain, because that
would require the primary one to load the assembly as well, which
prevents subsequent unloading. Unfortunately, I needed to be rather
abstract in my method of dealing with plugin classes, so everywhere I
had earlier made use of Type objects, I replaced them with string
parameters containing the fully-qualified classnames, and then did a
Type lookup in the secondary AppDomain with reflection. This seems to
work pretty well, but my CreateInstance method doesn't work.
Debugging in the code shows that the object is successfully created in
the second AppDomain, but trying to return the object to the primary
AppDomain results in a unintentional failed attempt to load into the
primary AppDomain the plugin assembly in which the object resides. I
tried a host of different methods of returning the object, and
everyone results in an error when the return value tries to cross the
AppDomain boundaries. The object derives from
MarshalByReferenceObject. (Although I think the CreateInstance
methods return a value of type object, not sure if that's a problem.
I probably ought to make it that way even if it's not the source of
the problem.) Also, I'm using an abstract base class instead of an
interface. Most plugin systems I looked at used an interface, but an
abstract class was really going to be far more useful to me, and I
assume that ought not to cause problems.

Re: Plugin difficulties David Levine
11/19/2004 12:30:07 AM
Here's something you could try...

Create a class, RemoteProxy, that proxies the calls to the remoted object.
The class would be defined in the same assembly as MainClass in the primary
appdomain, but gets loaded into the context of the remote appdomain. This is
essentially a call forwarder class.

Structurally it looks like this:

AssemblyMain (contains classes)
MainClass
RemoteProxy

Plugin1
Plugin2,etc.

In terms of appdomains

DefaultAppdomain
MainClass
A reference to RemoteProxy

Plugin1 Appdomain
RemoteProxy
Plugin1

Plugin2 Appdomain
RemoteProxy
Plugin2

When you want to create a plugin, you call a method in RemoteProxy that
takes as arguments the plugin assembly to load.

All calls to the plugins from MainClass are done via making a call to the
RemoteProxy which in turn forwards it to the plugin. When you unload the
plugin it also unloads the RemoteProxy. You need to do is ensure that all
objects defined in the plugin assembly are used only within the RemoteProxy
class and not in the MainClass.

This provides isolation between the main class and all the plugins.


[quoted text, click to view]

Re: Plugin difficulties vacindak NO[at]SPAM gmail.com
11/19/2004 12:37:03 PM
[quoted text, click to view]

This was pretty much the exact solution I was specifically trying to
avoid, dreading that I might have to actually do it. Fortunately, the
actual problem just turned out to be more stupidity on my part.

[quoted text, click to view]

I went ahead and set the CreateInstance methods to insist only on
types of MarshalByReferenceObject for return values. To my surprise,
I got a ClassCastException because... duhn dhun da dhun... in my
experiments I switched from using a base class derived from
MarshalByReferenceObject to an underived object with a [Serializable]
attribute attached. Everything started working exactly as intended
once I switched to a base class derived from MarshalByReferenceObject.
Apparently .NET doesn't bother to give reasonable error messages for
this sort of problem either, just a cryptic "couldn't find the
assembly blah, blah" error. Is there any particular reason why all
exceptions that come out of AppDomain-related stuff all semm to be
either SerializationException or FileNotFoundException, with
flagrantly deceptive error messages? Is there any reason we can't
Re: Plugin difficulties vacindak NO[at]SPAM gmail.com
11/19/2004 9:57:18 PM
Having gone through AppDomain hell and managed to survive somehow, I
thought I'd make sure that anyone who follows won't have quite the
crazy 3 weeks or so of banging their heads against the wall that I
did. As such I've written an article on CodeProject.com on the
subject which can be found through here:

http://www.codeproject.com/script/Articles/list_articles.asp?userid=312502

If there's some closely related area you'd like to see the article
expanded on, let me know. I'm currently thinking about adding runtime
AddThis Social Bookmark Button