Creating a Wix# Installer that Includes Prerequisites Part 3

Creating Custom Actions to Simplify Tasks

Introduction

In part 3, our installer gets a little more complicated. Having insured .Net 4.7 was installed in part 2, we are going to script out the install of Apache Tomcat as a Windows Service by utilizing Custom Actions.

The parts of this series are:

  1. Creating an MSI Installer with Wix#
  2. Creating an EXE Installer that Bundles Prerequisites
  3. Creating Custom Actions to Simplify Tasks (this)

Background

WiX allows MSI and EXE installers to do more than run executables and copy files. A CustomAction allows you to write various forms of actions that can be used for actions like backing up and restoring application settings during an upgrade, setting up prerequisites, or reporting an install to a remote server.

Assumptions

I am going to assume a few things going forward. If these are not true, your experience may differ.

  1. WiX Toolset is installed
  2. You are coding in C# (VBwww.NET will have similar principals but will require some translation)
  3. You have read over part 2. One of the actions we are going to use will fail if the required runtime is missing.

Getting Started

In this scenario we need to install a prerequisite such as Apache Tomcat for our application from the MSI and not the bootstrapper. This is accomplished differently from using a separate installer as we would from the bootstrapper.

private static string BuildMsi()
{
    var TestDir = new Dir(@"%ProgramFiles%\\My Company\\My Product");

    TestDir.Files = System.IO.Directory.GetFiles(@"D:\\Checkouts\\WixSharp Tutorial\\WixSharp Tutorial\\SampleApp\\bin\\Release").Where(file => file.EndsWith(".dll") || file.EndsWith(".exe")).Select(file => new File(file)).ToArray();

    var project = new Project("MyProduct", TestDir);

    project.GUID = new Guid("6fe30b47-2577-43ad-9095-1861ba25889b");
    //project.SourceBaseDir = "<input dir path>";
    //project.OutDir = "<output dir path>";

    project.Actions = new WixSharp.Action[] {          
        new ElevatedManagedAction(CustomActions.CheckForApache, "%this%"),
    };

    return project.BuildMsi();
}

Notice above that we have added a new Section to the code. project.Action adds a list of actions that can be defined in anumber of ways. The WiX ways are primarily vbs or Jscript, but for the .NET developer Managed Actions will feel more familiar and be easier to upkeep. We have defined these Actions in a separate class below.

public class CustomActions
{
    [CustomAction]
    public static ActionResult CheckForApache(Session session)
    {
        //check for apache
        return ActionResult.Success;
    }
}

The CustomAction Attribute marks these methods as an entry point for the installer. This is not a feature of WiX/Wix# however so you will need to add a using statement to the top of you file.

using Microsoft.Deployment.WindowsInstaller;

Implementing the Custom Actions

Below are code samples for how one might choose to implement the Custom Actions such as the one above.

Checking for Apache

public class CustomActions
    {
        [CustomAction]
        public static ActionResult CheckForApache(Session session)
        {
            ActionResult result = ActionResult.Success;
            try
            {
                System.ServiceProcess.ServiceController apache = ServiceController.GetServices()
                    .FirstOrDefault(s => s.ServiceName == "Tomcat8");

                if (apache == null)
                {
                    if (System.IO.File.Exists(@"C:\\ProgramData\\chocolatey\\choco.exe"))
                    {
                        CmdRunner(@"@"" % SystemRoot %\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"" - NoProfile - InputFormat None - ExecutionPolicy Bypass - Command ""iex((New - Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))"" && SET ""PATH =% PATH %;% ALLUSERSPROFILE %\\chocolatey\\bin");
                    }

                    CmdRunner("Choco install Tomcat");
                }
            }
            catch
            {
                result = ActionResult.Failure;
            }

            return result;
        }

        public static void CmdRunner(string cmd)
        {
            System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
            startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
            startInfo.FileName = "cmd.exe";
            startInfo.Arguments = @"/C" + cmd ;
            System.Diagnostics.Process process = new System.Diagnostics.Process();
            process.StartInfo = startInfo;
            process.Start();
            process.WaitForExit();
        }
    }

There are a few problems with the code above. We are forcing our user to install Chocolatey (a package manager for windows), we are checking for the default service name, and we are installing a prerequisite that is available as an installer (here). This means we should instead bundle it in the bootstrapper.

Points of Interest

  • Wix# Custom Actions come in a variety of flavors as demonstrated on github but the ManagedAction and ElevatedManagedAction being easier to deal with for .NET developers.
  • Custom Actions are only usable in MSI's and not bootstrappers.
  • The WiX Toolset Documentation for CustomAction Elements gives explicit detail on many of the options such as Return and Execute.

Customizing Custom Actions

Custom Actions can be more than what we have shown above. For instance, what if you want to start the application immediately after installing it?

new InstalledFileAction("sample_exe", "", Return.asyncNoWait, When.After, Step.InstallFinalize, Condition.NOT_Installed),

This indicates that a file installed by this MSI will be run After the InstallFinalizes and it will continue to run even after the installer exits, but only if the application was not already installed.

Key elements that assist in deciding when and what are:

  • Return tells the MSI if it needs to handle the actions completion and if so, what to do with it.
  • Execute determines when the CustomAction is scheduled to execute such as on RollBack of a failed MSI.
  • When combined with Step indicate in a relation in time to when the step should be executed.

Dealing With Errors

Be sure to appropriately wrap you CustomActions in a try/catch and handle the errors appropriately. Users may need to be notified of errors and you will want to fail the action if a vital portion of the MSI does not execute due to exceptions. Your choices for notifying the user may include a popup error and/or event log messages.

History

Did you find this article valuable?

Support Curtis Carter by becoming a sponsor. Any amount is appreciated!