Tuesday, June 06, 2006

Self installing services in .NET

I have some service applications that I deploy with Wise for Windows. These particular services are .NET assemblies. The usual way of registering the .NET assembly as a service is to use the installutil.exe that comes with the .NET Framework. Wise made it easy to register the assemblies by adding a checkbox in the file properties for self installation. Behind the scenes, Wise must be calling installutil, because it fails when you have multiple versions of the .NET Framework installed. Installutil is not compatible across Frameworks. You can’t install a 1.1 assembly with the 2.0 installutil, and vice versa.

Wise does not let you specify which version of the Framework is being used by a particuliar assembly. It should be able to tell through Reflection, but it doesn’t. This means I can’t specify the correct installutil to use for my services. This is not good and causes my install projects to go down in flames. I really can’t wait for Wise to fix this.

I could call installutil directly, but that means putting all sorts of fugly code into the install project to correctly locate the appropriate version of installutil. And that code would probably break the minute Microsoft updates the .NET Framework. So we move to Plan B, self-installing services. You would think that this would be a simple walk through the MSDN garden, but their code examples assume that that task is being handled manually via installutil or through a Windows Installer project.

After a bit of Googling, I found a reference to an undocumented method call, InstallHelper, in the System.Configuration.Install.ManagedInstallerClass class. By using this method, I can install or uninstall the service from the command line.

I augmented the Main() function in the service class to look like this:


static void Main(string[] args)

{

if (args.Length > 0)

{

if (args[0] == "/i")

{

System.Configuration.Install.ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });

}

else if (args[0] == "/u")

{

System.Configuration.Install.ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location });

}

else if (args[0] == "/d")

{

CollectorService MyService = new CollectorService();

MyService.OnStart(null);

System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);

}

}

else

{

System.ServiceProcess.ServiceBase[] ServicesToRun;

ServicesToRun = new System.ServiceProcess.ServiceBase[] { new CollectorService() };

System.ServiceProcess.ServiceBase.Run(ServicesToRun);

}

}


The “/d” part hasn’t been tested yet. That should allow me to debug the service as an application from within the Visual Studio IDE. As much as I dislike having to use an undocumented class, I’m not going to lose any sleep over it. Microsoft obsoleted documented functions going from Visual Studio 2003 to 2005, I’m not going to worry about one method.

[Edited on 6/8/06]
I updated the block of code for the "/d" part. I needed a timeout to keep the service running, otherwise it just runs through the startup and then exits. You can make it fancier, I just use that code for testing from within the IDE and I can break out of the service when I am done testing it.

[Edited on 7/20/06]
After a few go arounds with Wise Technical Support, I sent them a sample installer project that easily duplicate this bug and they did confirm that it was a problem with their current product. There is also a similiar problem where you can't install .NET 1.1 services under similiar circumstances. Their fix for my problem will fix the .NET 1.1 service problem too. According to the email that I had received, this is tentatively scheduled for the next release. That would probably be the version 7.0 release. In the meantime, I'll stick with my work around.

[Edited on 1/27/08]
The MyService object in the above code is an instance of a System.ServiceProcess.ServiceBase descendant class that I created in my code.  The descendant class opens up access to the protecteded OnStart() method.  I had created a descendant to ServiceBase and had assumed that was the standard pattern.  I should have been more clear about that part.  This is one of the many reasons why I abandoned Wise for InstallAware.

15 comments:

  1. Thanks. I'm going to steal your code now... Quite disappointed in the latest Wise product, which used to be so good. Seems like the Altiris acquisition was pretty much the beginning of the end though...

    ReplyDelete
  2. I don't know if the Altiris acquisition help or hurt Wise. Their stuff has alwys been a little flakey. I learned to use the non-flakey bits and implement my own work arounds for the other stuff. The Windows Installer core stuff has always as designed for me, it's the additional stuff that's always a bit dodgy. There was really no excuse for the stumbles with the .NET 2.0 stuff. They had plenty of time to prepare for it.

    ReplyDelete
  3. This is a slick idea and I was all jazzed to try it out. Unfortunatly, I am not certain if something changed since your posting, but this doesn't work in VS 2005 today. The call to MyService.OnStart() will fail because the ServiceBase.OnStart() is defined as protected, which hides it from the Program.Main() function, resulting in the compile-time error "'System.ServiceProcess.ServiceBase.OnStart(string[])' is inaccessible due to its protection level."

    Since you can't change the access modifiers on MyService.OnStart() without declaring MyService.OnStart() as new, I don't see how this can work. Defining MyService.OnStart() as new will cause the MyService.OnStart() method to not be called by any SCM function that casts the MyService class as ServiceBase.

    Any ideas?

    ReplyDelete
  4. Tony, This code compiled with VS 2005 and VS 2008. What was not clear from this code is that the MyService object is an instance of a class that I descended from System.ServiceProcess.ServiceBase. That provides the access to the OnStart method.

    ReplyDelete
  5. Thanks for posting this.

    I added the code for /i and /u in the main event. After I run it all the logs seem to be OK but the service does not appear in the services browser in admin tools???

    ReplyDelete
  6. Don't pay any attention to the comment I left above, the issue was that I did not include an installer in the project...

    ReplyDelete
  7. Worked before on XP, now doesn't on Vista: "System.Security.SecurityException:The source was not found, but some or all event logs could not be search."
    Do you think it's due to administrative access missing?
    Thank you,
    Gabriel

    ReplyDelete
  8. With Vista and Server 2008, installers run at a higher level of security access. Since it was an undocumented method call, you live and die by it. After dumping Wise For Windows for InstallAware, I no longer need to have to call ManagedInstallerClass.

    Did you try running your service as admin?

    ReplyDelete
  9. Thanks for your answer Chris. I'm new in Vista's world... It works very well when I right-click my .exe and "Run as admin". However my programme should work on any computer on any network. This means I have to deal with the "elevate user rights" on Vista. Doesn't seem so easy...
    Anyway, thanks for your quick reply!
    Gabriel

    ReplyDelete
  10. I am trying to port this to VB2008. Everything was going well until I hit the CollectorService reference. I have hunted around the internet and found nothing and, of course, VB doesn't recognize it.

    Any thoughts? Thanks.

    ReplyDelete
  11. The CollectorService reference is part of the code that made up the service. If you are writing a service application, you'll have code similar to that.

    ReplyDelete
  12. Senor Plankton10/05/2010 9:26 AM

    Can I please nominate this as The Most Useful Page On The Internet?

    Seriously though, this is excellent stuff.

    ReplyDelete
  13. This works brilliantly on VS 2010 - I agree with Senor Plankton!

    ReplyDelete
  14. Regarding the Collector Service, you can substitute (if your service is called "Service1":

    CollectorService MyService = new CollectorService();

    by

    Service1 MyService = new Service1;

    For me, it works !!! and the same for:

    ServicesToRun = new System.ServiceProcess.ServiceBase[] { new CollectorService()

    Change to:

    ServicesToRun = new System.ServiceProcess.ServiceBase[] { new Service1();

    Hope this helps
    Manuel

    ReplyDelete
  15. An alternative to using the command line args is to just check and see if the service is registered:

    ServiceController ctl = ServiceController.GetServices().Where(s=>s.ServiceName == "myservice").FirstOrDefault();
    if(ctl==null)
    //install the service here instead
    else

    ReplyDelete

Note: Only a member of this blog may post a comment.