Thursday, June 12, 2014

Writing a Useful Windows Service in .NET in Five Minutes

This helpful blog post about writing a Windows Service didn't allow comments so I'm writing a comment here.

I don't like to have to call external utilities to install and uninstall things; I think programs should be able to install and uninstall themselves, so I tried to figure out how to write Install() and Uninstall() methods using .NET code. Here's what I came up with:
[System.ComponentModel.RunInstaller(true)]
public class MyServiceInstaller : Installer
{
    private ServiceProcessInstaller processInstaller;
    private ServiceInstaller serviceInstaller;

    public MyServiceInstaller()
    {
        processInstaller = new ServiceProcessInstaller(); // "to write registry values associated with services you want to install."
        serviceInstaller = new ServiceInstaller();        // "to write registry values associated with the service to a subkey within the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services registry key"

        processInstaller.Account = ServiceAccount.LocalSystem;
        serviceInstaller.StartType = ServiceStartMode.Automatic;
        serviceInstaller.ServiceName = ServiceBaseClassName.Name;

        Installers.Add(serviceInstaller);
        Installers.Add(processInstaller);
    }

    public static void Install() // throws on failure
    {
        // MyServiceInstaller has an Install() method but we can't call it 
        // directly, because a NullReferenceException occurs in its bowels.
        //var installer = new MyServiceInstaller();
        
        // So instead we will ask AssemblyInstaller to scan this assembly,
        // and call Install() on our behalf.
        var installer = new AssemblyInstaller(Assembly.GetExecutingAssembly(), new string[] { "/ShowCallstack" });
        // Doc says: «Setting this property to true creates a new file named 
        // "{Assembly name}.InstallLog" to log messages for this assembly. Setting 
        // UseNewContext to false prevents the new file from being created.»
        installer.UseNewContext = true;

        var pointlessComplication = new Hashtable(); // who knows why
        // The documentation states "If all the Install methods succeed, the 
        // Commit method is called. Otherwise, the Rollback method is called."
        // That is WRONG, Install() does not call Commit().
        installer.Install(pointlessComplication);
        // However it probably doesn't matter if we call Commit() or not 
        // because ServiceInstaller and ServiceProcessInstaller do not override 
        // the Commit() method.
        installer.Commit(pointlessComplication);
        // Note: the service is not started yet (no, I don't know how.)
    }
    public static void Uninstall()
    {
        // Likewise, don't call Uninstall(), it throws NullReferenceException.
        // The exception that occurs on failure is wrong. For instance if the
        // service is not installed, the debugger tells you that an exception
        // occurs with the following message:
        //   "The specified service does not exist as an installed service."
        //
        // However, this exception is discarded (not preserved as an 
        // InnerException as it should be). Instead you get two nested 
        // InstallExceptions and both of them contain this brain-dead message:
        //   "An exception occurred while uninstalling. This exception will be 
        //   ignored and the uninstall will continue. However, the application 
        //   might not be fully uninstalled after the uninstall is complete."
        // 
        // The real exception is appended in the *.InstallLog file, although
        // there is no clear separation in that file between the install 
        // information and the uninstall information (e.g. no newlines).

        //var installer = new MyServiceInstaller();
        var installer = new AssemblyInstaller(Assembly.GetExecutingAssembly(), new string[] { "/ShowCallstack" });
        installer.UseNewContext = true;
        var pointlessComplication = new Hashtable();
        installer.Uninstall(pointlessComplication);
        installer.Commit(pointlessComplication);
    }
}
As you can see, I had an (undocumented) problem with NullReferenceException, but I worked it out.

Install() seems to work perfectly (the service appears on the service list, and no exception occurs), but Uninstall() doesn't work properly; the service is uninstalled, yet an exception occurs that says "Service was not found on computer '.'.". This exception is discarded by the uninstaller, then the uninstaller deletes the ServiceName.InstallState file, then it throws the following idiotic exception: "Could not find file 'C:\...\bin\x86\Debug\ServiceName.InstallState'."

Further investigation suggests that something silently went wrong during the install process, even though ServiceName.InstallLog does not show any problems. After a "successful" installation, running "C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil" /u C:\...\bin\x86\Debug\ServiceName.exe produces the following warning:
The file containing the saved state for the C:\...\bin\x86\Debug\ServiceName.exe assembly, located at C:\...\bin\x86\Debug\ServiceName.InstallState, could not be read, and the file might have been corrupted. The uninstall will continue without the saved information.
Huh. Okay. So next, based on a decompilation of System.Configuration.Install.ManagedInstallerClass.InstallHelper(), I basically uninstalled the same way it does:
public static void Uninstall()
{
    var installer = new AssemblyInstaller(Assembly.GetExecutingAssembly(), new string[] { "/ShowCallstack" });
    installer.UseNewContext = true;

    TransactedInstaller tInstaller = new TransactedInstaller();
    tInstaller.Installers.Add(installer);

    tInstaller.Uninstall(null);
}
Internally, the same exception occurs, "Service was not found on computer '.'." But the exception is caught and discarded and, as before, the service actually is uninstalled. Thanks TransactedInstaller! Unfortunately, using TransactedInstaller to perform the installation doesn't make any difference (if you use TransactedInstaller to perform the installation, InstallUtil /u still claims that the InstallState "might have been corrupted".)

Finally I tried this simple version:
public static void Install() // throws on failure
{
    ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location, "/ShowCallStack", });
}
public static void Uninstall()
{
    ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location, "/ShowCallStack", "/u" });
}
If you decompile InstallUtil you'll find that it's an "empty shell"; all it does it call this InstallHelper() method! However, still this code does not behave quite the same way as the real InstallUtil.exe; in fact it behaves exactly as before: the install and uninstall both succeed, but the uninstall involves a swallowed "Service was not found on computer '.'." exception and if you use InstallUtil.exe /u to uninstall, there is a message saying that the InstallState "could not be read". And again, if uninstall fails, the exception that contains the actual reason is thrown away and replaced by a generic message.

0 Comments:

Post a Comment

<< Home