Listing 2, below, shows a portion of the
ServiceBroker.StartService method, which is the core method for the application
functionality. You can see it takes one parameter, filePath, that is used to
locate the desired DLL. First, we extract the file name out of the path and
compare it to two files that will always be in the Service Manager directory.
We do this to avoid unnecessary processing for these files.
Listing 2 – StartService
public void StartService(string filePath)
{
string serviceName;
string fileName = filePath.Substring(
filePath.LastIndexOf("\\")
+ 1);
if (fileName.IndexOf("Microsoft.ApplicationBlocks")
!= -1 ||
fileName == "ServiceBroker.dll")
return;
try
{
AssemblyName asmName = null;
try
{
asmName = AssemblyName.GetAssemblyName(filePath);
}
catch (Exception ex)
{
Logger.WriteToLog(
String.Format("Could not get assembly name from '{0}'; bypassing that
file.",
filePath) + "
Exception Details: " + ex.ToString(),
System.Diagnostics.EventLogEntryType.Warning);
return;
}
…
Next we begin the loading process. We first use Reflection's
AssemblyName.GetAssemblyName method to do two things. We want to determine if
this is a .NET assembly or some other kind of DLL. If an exception is thrown
from that method, it is most likely not a .NET assembly. We also will be using
some of the AssemblyName information later. Please note that loading the
AssemblyName does not load the assembly into memory-it only reads the metadata
necessary to populate the AssemblyName properties.
In Listing 3, we see the next block of code in this method.
Its purpose is to see if we can shortcut further execution by seeing if we have
already loaded the current version of the assembly in question. It does this by
checking first if we've cached it in the serviceNames collection. If so, we get
the service name and the last modified time from the file currently in the
directory. We use these values to compare to the cached last modified time to
see if a new version has been dropped into the directory.
Listing 3 – StartService Cont’d
if (this.serviceNames.Contains(filePath))
{
serviceName = this.serviceNames[filePath].ToString();
DateTime curTime = File.GetLastWriteTime(filePath);
if (curTime.Ticks <=
((DateTime)this.serviceLastModified[serviceName]).Ticks)
{
Logger.WriteToLog(
String.Format("Skipping
'{0}' because it is already loaded.",
serviceName),
System.Diagnostics.EventLogEntryType.Information);
return;
}
else {
this.UnloadService(serviceName);
this.serviceNames.Remove(filePath);
}
}
If the version currently in the directory is the same as or
older than the currently-loaded version, we log that we are skipping it and
exit out of the function because we don't need to reload. Otherwise, a new
version has been placed in the directory, so we need to unload the current
version and remove it from the caches.
The UnloadService method (Listing 4) is used anywhere that
we want to stop and unload a managed service and remove references to it from
our static caches.
Listing 4 – UnloadService
private void UnloadService(string serviceName)
{
RemoteServiceHandler service =
this.services[serviceName] as RemoteServiceHandler;
if (service != null)
{
try
{
service.StopService();
}
catch (Exception ex)
{
Logger.LogException(ex);
}
}
this.services.Remove(serviceName);
AppDomain svcDomain =
this.serviceAppDomains[serviceName] as
AppDomain;
if (svcDomain != null)
{
try
{
AppDomain.Unload(svcDomain);
}
catch (Exception ex)
{
Logger.LogException(ex);
}
}
this.serviceAppDomains.Remove(serviceName);
this.serviceLastModified.Remove(serviceName);
}
Using the service name, we attempt to get the reference to
the managed service's RemoteServiceHandler in order to shut it down by calling
its IService.StopService implementation-RemoteServiceHandler.StopService calls
that on whatever service it is handling. We'll go over the RemoteServiceHandler
details shortly. The next thing this method does is get a reference to the
managed service's AppDomain and attempts to unload it using AppDomain.Unload,
and finally, it removes the other references in the caches, except for the
serviceNames cache, which must be cleared in the calling code as seen in the
last line of Listing 3.
After checking if we need to load a new version of the
managed service (and unloading if it's already there), we move on to the code
required to create our AppDomain and remotely load the managed service.
Listing 5 – StartService Cont’d
AppDomain svcDomain = null;
try
{
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
setup.PrivateBinPath =
setup.ApplicationBase;
setup.ApplicationName = asmName.FullName;
setup.ShadowCopyDirectories =
setup.ApplicationBase;
setup.ShadowCopyFiles = "true";
svcDomain = AppDomain.CreateDomain(asmName.FullName,
null, setup);
}
catch (Exception ex)
{
Logger.LogException(
new ApplicationException(
String.Format("Could not create an
AppDomain for '{0}'; bypassing that assembly.",
asmName.FullName), ex));
return;
}
The listing above shows the code we use to set up a new app
domain to be used for running the managed service. I decided to use an
AppDomainSetup object because it provides some options that the
AppDomain.CreateDomain overloads do not. The important items here are to set
the directory properties to point to the current application directory and to
set it to shadow copy the files so that it will not lock the assemblies in the
application directory. I wouldn't expect an exception to be thrown here
normally, but I thought I'd catch it to log exactly what was taking place and
to not end the thread in an exception. This is a good practice when your
applications do not have a user interface; that is, you need to catch and log
exceptions.
Now we have our very own AppDomain created for our managed
service to run within, so the next thing to do is to load up the managed
service in the new application domain. To do this without loading any type data
into the main AppDomain, we have to use the aforementioned
RemoteServiceHandler. What this type does is give us a known type (it's
declared in the ServiceBroker assembly, not the managed service assembly) that
we can use to instantiate in the child application domain. Since it inherits
from MarshalByRef, the instance is actually hosted in the child domain, and we
simply send messages to it.
Listing 6 – StartService Cont’d
RemoteServiceHandler svc = null;
try
{
svc = (RemoteServiceHandler)
svcDomain.CreateInstanceFromAndUnwrap(
svcDomain.BaseDirectory + "\\ServiceBroker.dll",
"ServiceBroker.RemoteServiceHandler");
}
catch (Exception ex)
{
AppDomain.Unload(svcDomain);
Logger.LogException(
new AssemblyLoadException(
"Could not load ServiceBroker remote
service handler, bypassing that file.",
asmName.FullName, ex));
return;
}
Listing 6 shows the code that will attempt to create an
instance of the RemoteServiceHandler to be hosted in the managed service's
domain. We do this by calling the CreateInstanceFromAndUnwrap method on that
AppDomain instance. This particular method is just a handy helper because
essentially it just does what two other methods available to us would do,
namely AppDomain.CreateInstanceFrom and ObjectHandle.UnWrap. CreateInstanceFrom
returns an ObjectHandle; then UnWrap is called on the ObjectHandle, which gives
us a transparent proxy to the RemoteServiceHandler in the service's domain.
You could say that at this point we know of a person in a
nearby office and have her address on hand for interoffice memos. But everybody
else involved in this operation, thus far, has been in the same room with us.
This is the first remote person we're dealing with.
Notice also that we start calling AppDomain.Unload in our
exception handlers at this point. This is because we now have a loaded
application domain that we don't want to keep in memory should something go
wrong that makes the AppDomain no longer necessary.
There are a couple things to consider here about
RemoteServiceHandler that relate to its Remoting capabilities. First, we set it
to inherit from MarshalByRef; I won't reiterate here what has been said on this
point already. Also, and quite significantly, we override the InitializeLifetimeService
to return null. The reason for this is that we do not want our proxy to expire
or our remote object to be collected. Returning null from this method
effectively disables lifetime management for the instance. We want to maintain
a viable link to the managed service we are creating because we will want to
call the StopService method on it at some later time, possibly weeks or even
months down the road.
The next step we make, once we have acquired a transparent
proxy to a RemoteServiceHandler instance in the service's AppDomain, is to go
ahead and attempt to start up the service in its domain. We do this by calling
RemoteServiceHandler's LoadService method, as seen in the next listing. Doing
this actually sends a message to the instance in the other AppDomain, asking it
to load a service with the name we provide, just as we might send a note to a
coworker in another office to ask them to help us out with something. Remember,
any time we are dealing with a proxy, we are not actually dealing directly with
the instance but are, rather, sending messages back and forth.
Listing 7 – StartService Cont’d
try
{
if (!svc.LoadService(asmName.FullName))
{
AppDomain.Unload(svcDomain);
Logger.WriteToLog(
String.Format("No ServiceEntryPointAttribute
was found for assembly '{0}'.",
asmName.FullName),
System.Diagnostics.EventLogEntryType.Warning);
return;
}
}
catch (Exception ex)
{
AppDomain.Unload(svcDomain);
Logger.LogException(ex);
return;
}
LoadService does three, critical things. First, it attempts
to load up the target assembly. In our case, it uses the AssemblyName.FullName
property that we retrieved earlier when determining that this is a .NET
assembly. After loading the managed service assembly, it attempts to find the
ServiceEntryPointAttribute for that assembly using Reflection's
Assembly.GetCustomAttributes method. Assuming the loaded assembly has a
ServiceEntryPoint attribute applied to it, LoadService can then retrieve the
service entry point type name and the service friendly name. Lastly, with the
information from that attribute, it attempts to create an instance of the type
that implements IService, calling CreateInstance on the assembly and passing in
the service entry point type name as shown below in Listing 8.
Listing 8 – RemoteServiceHandler.LoadService
(Excerpt)
try
{
this.service = (IService)assembly.CreateInstance(
this.serviceEntryPointType, true);
}
catch (Exception ex)
{
throw new TypeInitializationException(
this.serviceEntryPointType, ex);
}
If all of this works without exception, the method returns a
true value (by sending a message back to ServiceBroker), indicating that the
managed service in the specified assembly was loaded correctly. By this point,
the bulk of the magic is done in the ServiceBroker. We now have a new
application domain in which we have remotely loaded an instance of the desired
managed service. All that is left to do now is to start the managed service and
cache our references so that we can use them later to stop and unload the service
when necessary, as seen in Listing 9.
Listing 9 – StartService Cont’d
svc.StartService();
serviceName = svc.ServiceName;
this.serviceNames.Add(filePath, serviceName);
this.services.Add(serviceName, svc);
this.serviceLastModified.Add(
serviceName, File.GetLastWriteTime(filePath));
this.serviceAppDomains.Add(serviceName,
svcDomain);
After the code in Listing 9 executes, the service will have
started, i.e., whatever code that the author of that particular managed service
chose to include in his IService.StartService implementation will have been
executed. In Part 1, we covered what
is involved in setting up a managed service, and in our particular
SampleService, all that would have executed was a message logged to the event
log, but I imagine that for most normal managed services, a timer of some sort
will be started to handle a task on a regular basis.