Continued from “Building a Windows Service – Part 4: Extending the Service Installer

So with our installer working it would be nice to have a way to just run the install right from the command-line. There are several options here, one way would be to use the Process object, set the start parameters to not use shell execute, and just run and wait for exit. The other option, though a little ‘hackish’ is to leverage the fact that InstallUtil itself is a managed application. Because it is managed we can simply take the Assembly’s full path and hand it over to the AppDomain.ExecuteAssembly method. The nice thing about this approach is that we can capture and redirect output (via Console.SetOut()) or just let it do it’s thing. In addition, we have the luxury of not needing to encode and escape the command-line arguments, instead we just supply them as a string array. So let’s build a little utility class to deal with this for us:

class InstallUtil
{
    private readonly AssemblyName _assemblyName;
    private readonly string _baseDirectory;
    private readonly string _fileName;
    private readonly string _installUtilExe;

    public InstallUtil(Assembly assembly)
    {
        _assemblyName = assembly.GetName();
        _fileName = Path.GetFullPath(assembly.Location);
        _baseDirectory = Path.GetDirectoryName(_fileName);

        _installUtilExe = Path.GetFullPath(Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "InstallUtil.exe"));
        if (!File.Exists(_installUtilExe))
            throw new FileNotFoundException("InstallUtil.exe not found.", _installUtilExe);
    }

    public void Install(IEnumerable<string> moreargs)
    {
        if (0 != Run(true, moreargs))
            throw new ApplicationException(String.Format("InstallUtil failed to install {0}.", _assemblyName.Name));
    }

    public void Uninstall(IEnumerable<string> moreargs)
    {
        if (0 != Run(false, moreargs))
            Console.Error.WriteLine("InstallUtil failed to uninstall {0}.", _assemblyName.Name);
    }

    private int Run(bool install, IEnumerable<string> moreargs)
    {
        string apppath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
        string appdata = Path.Combine(apppath, _assemblyName.Name);
        if (!Directory.Exists(appdata))
            Directory.CreateDirectory(appdata);

        List<string> arguments = new List<string>(moreargs);
        if (!install)
            arguments.Add("/uninstall");
        arguments.AddRange(new []
                               {
                                   "/ShowCallStack=true",
                                   String.Format("/LogToConsole=true"),
                                   String.Format("/LogFile={0}", Path.Combine(appdata, "install.log")),
                                   String.Format("/InstallStateDir={0}", _baseDirectory),
                                   String.Format("{0}", _fileName),
                               });

        int result = AppDomain.CurrentDomain.ExecuteAssembly(
                _installUtilExe,
                AppDomain.CurrentDomain.Evidence,
                arguments.ToArray()
                );

        return result;
    }
}

And then we can simply crack open our Commands class from part 1 and add two more static methods:

public static void Install(ICollection<string> args)
{
    new InstallUtil(typeof (Program).Assembly)
        .Install(args);
}

public static void Uninstall(ICollection<string> args)
{
    new InstallUtil(typeof(Program).Assembly)
        .Uninstall(args);
}

Following that we add these methods to Program’s Actions dictionary and we are done. Now we can simply run our service exe with the commands we defined:

C:\Projects\ServiceTemplate> ServiceTemplate.exe install
C:\Projects\ServiceTemplate> ServiceTemplate.exe uninstall

Continued on “Building a Windows Service – Part 6: Adding resources and event logging

Comments