Running scripts from a Windows Azure role’s OnStart method

Summary: Startup scripts declared in ServiceDefinition.csdef work well in most cases, but if you need to modify IIS configuration you’ll need to run your scripts from your role’s OnStart method. This post includes some sample code that can do this using a configuration-driven declarative approach.

In a Windows Azure cloud service, you can specify scripts that should run at role startup time using the <Startup> element in ServiceDefinition.csdef. This works well in almost all cases, and as it’s declarative you don’t need to write any code (other than the scripts themselves, obviously). However there is one common scenario where startup scripts won’t cut it: anytime you need to query or modify your IIS configuration. Why? As Kevin Williamson depicts nicely in this post, the IISConfigurator does not create IIS websites (step 8 in Kevin’s post) until after the startup tasks have already finished (step 7). So if you want to do anything to IIS that can’t be defined declaratively in ServiceDefinition.csdef, you’ll need to do it at some point after the IISConfigurator has done its bit.

The logical place do this is in your role’s OnStart method (triggered in step 9). Normally you’ll do this via a call to Process.Start and associated code, and this is a fine approach – however compared to the declarative simplicity of “real” startup tasks, hard-coding the details into C# code makes me feel a little dirty, especially if you want to call more than one script or you’re doing this in multiple applications.

So I built a simple solution that lets you use a declarative approach to start processes and scripts from your role’s OnStart method. The full code can be downloaded here; the highlights are described below.

This solution lets you specify the scripts you want to run using a configuration setting defined in ServiceConfiguration.cscfg, for example:

 <ConfigurationSettings>
  <Setting name="RoleStartupScripts" value="startup\test1.cmd!;startup\test2.cmd;Powershell.exe startup\test3.ps1" />
    ....
</ConfigurationSettings>

The setting value contains a semicolon-separated list of scripts to run. Note that the first script has an exclamation mark after its filename – this is my way of saying that the code needs to wait for that process to finish before starting the next script. For scripts without the exclamation mark, it won’t wait. There is no way to specify that a script needs to run as elevated; if you need this (which will generally be the case for IIS manipulation), be sure to set <Runtime executionContext="elevated" /> in your ServiceDefinition.csdef file to force the role startup code (and all processes it launches) to be elevated.

Next, I built an abstract class StartupScriptRoleEntryPoint that extends RoleEntryPoint to automatically launch the processes specified in configuration. My class is directly in the WebRole project, but you’d probably want to move this into a reusable library.

 public abstract class StartupScriptRoleEntryPoint : RoleEntryPoint
{
    public override bool OnStart()
    {
        var startupScripts = CloudConfigurationManager.GetSetting("RoleStartupScripts");
        if (!String.IsNullOrEmpty(startupScripts))
        {
            var scriptList = startupScripts.Split(';');
            foreach (var script in scriptList)
            {
                bool waitForExit = false;

                string scriptCommandLine = script;
                if (script.EndsWith("!"))
                {
                    scriptCommandLine = script.Substring(0, script.Length - 1);
                    waitForExit = true;
                }

                var args = CmdLineToArgvW.SplitArgs(scriptCommandLine);

                var processStartInfo = new ProcessStartInfo()
                {
                    FileName = args[0],
                    Arguments = string.Join(" ", args.Skip(1).ToArray()),
                    WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory,
                    UseShellExecute = true,
                };
                var process = Process.Start(processStartInfo);
                if (waitForExit)
                {
                    process.WaitForExit();
                }
            }
        }
        return base.OnStart();
    }
}

This code should be fairly self-explanatory – it loops through each specified script and launches it as a new process. The one unusual bit is the CmdLineToArgvW.SplitArgs call; this uses P/Invoke to call a Win32 API to parse the command line arguments out of a string which is necessary when launching a process from .NET.

Now all that’s left is to update the WebRole class to derive from StartupScriptRoleEntryPoint instead of the default RoleEntryPoint – no other code changes are needed. You could do this for a worker role too I guess, although I’m not sure if there’s a scenario where this is useful.

 public class WebRole : StartupScriptRoleEntryPoint
{
    public override bool OnStart()
    {
        // For information on handling configuration changes
        // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357.

        return base.OnStart();
    }
}

Since this approach uses configuration settings from ServiceConfiguration.cscfg, one interesting consequence is that you can change which scripts run in which environments – this may be particularly useful when running under the emulator on your local PC where you don’t want scripts doing weird things to your box. It’s also possible to change the configuration after deployment, but remember the settings are only used when the role starts (and any scripts you refer to need to already exist on the Windows Azure VM).