AppDomains


As described earlier in this chapter, an AppDomain is unit of isolation inside of a process. Most program failures can be contained inside a single domain. AppDomains do share some common resources, but failure in these shared components should be rare. The host, whether it is just the CLR or a more advanced host such as SQL Server or ASP.NET, will take care of managing AppDomains on your behalf. Very seldom will you have to worry about creating or managing them yourself, but if you must, the System.AppDomain APIs permit you to do so. Each AppDomain in a process can be manipulated through an instance of the AppDomain class.

Creation

The simplest way to get a reference to an active AppDomain is through the AppDomain.CurrentDomain static property. The object returned represents the AppDomain currently running the statement that accesses this property. Alternatively, you can create an entirely new AppDomain with a call to the AppDomain.CreateDomain(string friendlyName, ...) method.

CreateDomain offers several overloads, enabling you to pass such things as security Evidence, paths for assemblies and configuration files (discussed in Chapter 4), activation context and arguments, and much more. The AppDomainSetup class offers a complete list of possible settings:

 public ActivationArguments ActivationArguments { get; set; } public AppDomainInitializer AppDomainInitializer { get; set; } public string[] AppDomainInitializerArguments { get; set; } public string ApplicationBase { get; set; } public string ApplicationName { get; set; } public ApplicationTrust ApplicationTrust { get; set; } public string CachePath { get; set; } public string ConfigurationFile { get; set; } public bool DisallowApplicationBaseProbing { get; set; } public bool DisallowBindingRedirects { get; set; } public bool DisallowCodeDownload { get; set; } public bool DisallowPublisherPolicy { get; set; } public string DynamicBase { get; set; } public string LicenseFile { get; set; } public LoaderOptimization LoaderOptimization { get; set; } public string PrivateBinPath { get; set; } public string PrivateBinPathProbe { get; set; } public string ShadowCopyDirectories { get; set; } public string ShadowCopyFiles { get; set; } 

Refer to the SDK documentation for details on each of these.

Unloading

To unload an entire AppDomain — interrupting any executing code abruptly in the process — you can use the static void AppDomain.Unload(AppDomain domain) method. Passing in an active AppDomain as the argument to this method will ask the CLR to unload it and any resources (such as domain-specific assemblies) associated with it. This causes ThreadAbortExceptions to be raised in all threads that have stack in the target AppDomain.

The basic process of unloading an AppDomain involves (1) halting execution of each thread that has stack in the target AppDomain, (2) aborting each of these threads, (3) raising the AppDomain.Unload event, (4) running finalizers on unreachable objects (escalating to a rude shutdown if they take too long), and (5) freeing internal data structures and garbage collecting the entire contents of the domain. Thread aborts occur in the typical fashion, although it is entirely host dependent whether finally blocks will be run or not (i.e., a rude or polite thread abort).

Loading Code into an AppDomain

When an assembly is loaded, either through the AppDomain.ExecuteAssembly* or Assembly.Load* methods, it will be associated with the AppDomain in which it was loaded. There are two styles of loading: domain-specific and domain-neutral. The way in which an assembly is loaded will affect the way it is shared between AppDomains and, therefore, how it is unloaded. The specific details around domain specificity and neutrality are discussed in detail in Chapter 4.

Loading domain-specific is the default behavior for most assemblies. An assembly loaded in this fashion has an affinity to the AppDomain in which it lives, and it is not shared at all among other AppDomains. This has the consequence that, should multiple AppDomains inside one process need to access the same assembly, it will be loaded more than once into memory. When an AppDomain is unloaded, all of its domain-specific assemblies will be unloaded as well, freeing the memory associated with them. Unfortunately, a domain-neutral assembly never gets unloaded during the life of its enclosing process, not even when the domain that loaded it gets unloaded.

The AppDomain.GetAssemblies instance method returns an Assembly[] containing each assembly currently loaded in the domain. Similarly, ReflectionOnlyGetAssemblies returns an Assembly[] containing those assemblies loaded in the reflection-only load context.

Marshaling

There are several kinds of marshaling that can occur when sharing data across AppDomains. Which occurs depends on the type of the data being marshaled. Marshal-by-value is the default behavior for value types and reference types marked with the System.SerializableAttribute custom attribute. This does not preserve the identity of objects during marshaling, instead treating everything that crosses over into another AppDomain as an opaque sequence of bits that is deserialized on the receiving side. Marshal-by-reference, on the other hand, preserves the identity of the objects through the use of remoting proxy classes in the receiving AppDomain. This style of marshaling is used for any serializable type that derives from MarshalByRefObject.

Load, Unload, and Exception Events

A number of events are exposed by the AppDomain type, enabling you to react to momentous events in the lifecycle of an AppDomain:

 public event AssemblyLoadEventHandler AssemblyLoad; public event ResolveEventHandler AssemblyResolve; public event EventHandler DomainUnload; public event EventHandler ProcessExit; public event ResolveEventHandler ReflectionOnlyAssemblyResolve; public event ResolveEventHandler ResourceResolve; public event ResolveEventHandler TypeResolve; public event UnhandledExceptionEventHandler UnhandledException; 

Several events occur when resolving a component fails. They can be used for information purposes such as logging and monitoring, or to plug in custom resolution behavior. Each of these has an event handler of the form Assembly ResolveEventHandler(object sender, ResolveEventArgs e), where the event arguments contain only a single argument Name, a string representing the name of the component that failed to load. These events enable you to return an Assembly containing the thing being asked for, which will then be searched (or in the case of an AssemblyResolve event, used directly). Chapter 4 discussed how to use this technique for AssemblyResolve to invoke custom assembly binding behavior.

There are also a set of purely informational events. For example, AssemblyLoad event is triggered after an assembly is loaded into an AppDomain. Similarly, DomainUnload is fired as the AppDomain is being unloaded, and ProcessExit is called when the enclosing process exits.

There is a single exception-based event: UnhandledException, which is triggered if an exception is unhandled and reaches the top of a call stack in the AppDomain. The event handler for this is voidUnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e). The event arguments consist of two things: ExceptionObject, and IsTerminating, which indicates whether the unhandled exception has caused the CLR to terminate.

AppDomain Isolation

We've touched on AppDomains quite a bit in this chapter already. They are discussed in additional detail in Chapter 4, which considers them in relation to the assembly load process. Because AppDomains and assemblies are so tightly related, we will briefly go on a tangent to talk about how AppDomains can (and in some cases should) be used to isolate assemblies from one another.

Imagine a scenario where a host application needs to execute code inside a third-party plug-in. Plug-ins are standard extensions to an existing application that users might make use of for specialized or enhanced features that don't come with the primary application. This is a very common model for ISV applications and one that the .NET Framework's architecture makes quite simple. Assemblies are the perfect unit of deployment to encapsulate a plug-in.

AppDomains provide a great deal of isolation between code running in other AppDomains. This barrier creates a layer of protection between malicious or accidental code that might otherwise be damaging and the host application. Static state can't be affected, security permissions can be explicitly downgraded (for example, if you wanted to deny access to the plug-in to access the disk), and plug-in crashes won't crash the host application and can be dealt with in a controlled fashion.

An example of code that loads a plug-in that derives from a standard base class is shown in Listing 10-2.

Listing 10-2: Plug-in isolation using AppDomains

image from book
 void MainPluginLoop() {     List<AppDomain> plugins = new List<AppDomain>();     // Load our plugins     plugins.Add(LoadAndExecutePlugin("PluginA.dll", "FooPlugin"));     /* Do some interesting work */     // To unload plugins already loaded, this code does the trick     foreach (AppDomain ad in plugins)         AppDomain.Unload(ad); } AppDomain LoadAndExecutePlugin(string pluginBin, string pluginType) {     // Create a new AppDomain and assign it restricted permissions     bool failedToLoad = false;     AppDomain ad = AppDomain.CreateDomain("PluginIsolation!" + pluginBin);     ApplyPluginPolicy(ad);     // Load & execute the plugin     PluginBase plugin = null;     try     {         plugin = ad.CreateInstanceAndUnwrap(pluginBin, pluginType) as PluginBase;     }     catch (Exception)     {         // This code should actually tell the host why it failed     }     if (plugin == null)     {         // Unload the new AppDomain & return null to indicate failure         AppDomain.Unload(ad);         ad = null;     }     else     {         plugin.Run();     }     // Return the new AppDomain so the host can track it     return ad; } void ApplyPluginPolicy(AppDomain ad) {     // Enable plugins to execute code     PermissionSet rootPermissions = new PermissionSet(PermissionState.None); rootPermissions.AddPermission(     new SecurityPermission(SecurityPermissionFlag.Execution)); UnionCodeGroup rootGroup = new UnionCodeGroup(     new AllMembershipCondition(), new PolicyStatement(rootPermissions)); // Now locate the permissions for the "Internet" zone NamedPermissionSet internet = null; IEnumerator policyEnum = SecurityManager.PolicyHierarchy(); while (policyEnum.MoveNext()) {     PolicyLevel level = (PolicyLevel)policyEnum.Current;     if (level.Type.Equals(PolicyLevelType.Machine))     {         foreach (NamedPermissionSet permission in level.NamedPermissionSets)         {             if (permission.Name.Equals("Internet"))             {                 internet = permission;                 break;             }         }         if (internet != null)             break;         }     }     // Use those as the basis for plug-in CAS rights     UnionCodeGroup internetGroup = new UnionCodeGroup(         new ZoneMembershipCondition(SecurityZone.MyComputer),         new PolicyStatement(internet));     internetGroup.Name = "PluginInternet";     rootGroup.AddChild(internetGroup);     // Now just set the level on the AppDomain     PolicyLevel adLevel = PolicyLevel.CreateAppDomainLevel();     adLevel.RootCodeGroup = rootGroup;     ad.SetAppDomainPolicy(adLevel); } 
image from book

LoadAndExecutePlugin takes as input the binary name and type name of the plug-in to load. It sets up a new AppDomain and creates an instance of the plug-in type using the CreateInstanceAndUnwrap method. The plug-in type must inherit from the base type PluginBase:

 public abstract class PluginBase : MarshalByRefObject {     public abstract void Run();     /* Some interesting general plug-in methods go here */ } 

Notice that PluginBase inherits from MarshalByRefObject to keep the plug-in assembly from accidentally getting loaded into the host's AppDomain. This would be bad because assembly or module initializers would get run inside the host AppDomain, opening up the possibility of security holes.

The ApplyPluginPolicy code uses quite a bit of infrastructure available in the System.Security, System.Security.Permissions and System.Security.Policy namespaces to generate the appropriate code access security (CAS) permission sets for the AppDomain. It restricts the code loaded inside it from executing privileged operations. We discussed CAS in detail in Chapter 9. The end result is that the plug-ins can only execute operations that are granted to ordinary Internet applications. This isn't very much and certainly doesn't enable the plug-in to touch the disk.

Note that isolating each plug-in in its own AppDomain is not always necessary. Especially when considering that you might need to load lots of domain-specific code common to several plug-ins, causing quite a bit of duplicate code loading and an impact on working set. A reasonable compromise might be to isolate all plug-ins inside a single AppDomain or to utilize a pool of plug-in AppDomains. This means that plug-ins could step on each other, but it would at least protect against them corrupting the host application. Ultimately, this is a design tradeoff that must be made.

AppDomain-Local Storage (ALS)

Unless you use something like thread-static fields, each AppDomain contains a copy of all static fields. All class (or static) constructors will run once within a given AppDomain. This means that if you load the same assembly in different AppDomains, each will run the class constructors, and each will contain separate values for all static fields, for example.

Very much like thread-local storage (TLS), you can also store arbitrary data associated with a single AppDomain without necessarily having to use static fields. The capabilities aren't quite as advanced as with the Framework's support for TLS, as ALS supports a dictionary of named data slots. As with TLS, any data stored in an AppDomain is not visible from outside that AppDomain. To use ALS, you simply use the AppDomain instance methods object GetData(string name) and void SetData(string name, object data). The first of the two retrieves the data associated with name, or null if nothing exists under the specified key. SetData creates or alters an existing association between name and data.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net