Monday, 15 December 2008

Unable to determine the identity of domain

Don't you just hate it when you have some code that works fine in your development environment but not when you hand it to testers?

Loyal readers might recall that I have been migrating a bunch of legacy Win32 code to .Net. In the case of our GUI editor, that people use to map out their business processes, I have been writing a C# COM component that gets called from the legacy Delphi.Win32 code. Gradually more and more functionality is migrating to C#.

Along the way we have been storing the user's business process map in an OpenXML package. This is basically a ZIP file containing images, the model XML and bits of XML specifying how things are connected. Then it's a case of using System.IO.Packaging to get at it all.

Testing discovered that some larger client models were failing to upgrade with the mysterious error message "Unable to determine the identity of domain". This turns out to have nothing to do with Windows domains or user permissions.

As with all things .Net, someone somewhere has no doubt hit this problem before. Kevin Rohrbaugh had a similar scenario
http://www.coderoni.com/2008/04/04/server-side-office-document-generation-bug/
. His workaround was to ensure that they were running under an account that did not have a profile on the local machine (more below). That's not an option for us.

I'll spare you a gruesome recap of the full debugging which involves much "spelunking" with Reflector as Kevin so aptly calls it. Here are the broad strokes:

When you get a stream on a part in an OpenXML package (so that you can get at the uncompressed bits), you think you are getting something in-memory. However, if the package part is too big (more than 1.3Mb compressed), the framework decides to unzip the entire package part to disk and to give you a handle on that instead. This has unforeseen performance consequences, but we'll ignore them for now.

Where does it unzip them to? Isolated Storage. Exactly what kind of isolated storage depends on the user account you are running under. If you are running under a user account that doesn't have a profile on the local machine, the framework uses a machine-scoped location. If you are running under an account that does have a profile on the local machine (as we will be -- it'll be running under the account of the user of the GUI tool) it uses a user-scoped account.

If I add a reference to the assembly containing the code that accesses the package, and run it in the debugger all is well. But under COM all is far from well.

Running reflector on the Isolated Package class shows that when using user-scoped isolated storage, the framework examines the "evidence" of the AppDomain (where an AppDomain is a lightweight process). Under COM, we are running in a DefaultDomain that doesn't have any evidence.

You can't set the evidence for an AppDomain once it has been started and it's not possible to specify that the COM DLL should run with certain evidence or in a special AppDomain. Running the "Microsoft .NET Framework 2.0 Configuration" tool and granting Full Trust to our assembly doesn't solve the problem because there is still no "evidence" for the Framework code to examine. So there are two options:

1) In the Win32 code, host the CLR, create an AppDomain with the appropriate evidence and load the assembly. Use reflection to get at the methods.

2) (What I did in the interests of expediency). In the COM component, create a new AppDomain with the appropriate evidence. and execute the code in that. This works fine. There is a performance hit because we are now marshaling across AppDomains as well as marshaling across COM. We will see if the performance is acceptable. If not we will have to go with (1).

Doing (2) is similar to what you have to do for Office add-ins. For Office add-ins, the recommended strategy to satisfy the security model is to have an unmanaged shim. You sign the shim to make Office happy. Office talks to the shim, the shim acts as a proxy passing everything to your managed code.

In our case, the COM interface now loads up the AppDomain and proxies calls to an instance of our class running in that AppDomain.

The crucial (necessary and sufficient) piece of evidence is that we require the code to be running in the MyComputer zone.

First we need a simple AppDomainSetup:

AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory.ToString();


Then we need our evidence

Evidence evidence = new Evidence();
evidence.AddHost(new Zone(SecurityZone.MyComputer));


Now we can fire up an AppDomain running with that evidence.

AppDomain hostedAppDomain = AppDomain.CreateDomain("Demo", evidence, setup);


Now we get a handle on an instance of our class running in that AppDomain

ObjectHandle handle = hostedAppDomain.CreateInstance("MyStuff.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d0e8b069449d61a1", "MyStuff.Demo.DemoComponent");


To pull this off, DemoComponent has to inherit from MarshalByRefObject so now we have a little .Net remoting magic to do. We have to get a lease on the object and extend its lease if we are not done with it. A trivial class LicenseRenewer that implements ISponsor does the trick

lease = (ILease)handle.GetLifetimeService();
lease.Register(leaseRenewer);


Finally we can get a usable instance of the class. We access this locally and it transparently proxies calls to the other AppDomain.

demoComponent = (IDemoComponent)handle.Unwrap();


Now calls to our COM interface can just explicitly proxy to demoComponent e.g.

public bool Demo()
{
return demoComponent.Demo();
}


Then our COM interface just proxies things to demoComponent. Any types you want to marshall across AppDomains have to marked [Serializable()] of course.