The Perfect Service - Part 2
page 4 of 6
by J. Ambrose Little
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 28233/ 34

Start 'Em Up

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.


View Entire Article

User Comments

No comments posted yet.






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-18 8:30:16 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search