Поделиться через


Tracking and Controlling PowerShell Script Execution Progress via XML

Have you ever wanted to track the progress of one of your multi-step/stage PowerShell scripts on a computer, just in case during the middle of the steps the script (or PowerShell itself) terminates unexpectedly, or something randomly reboots the computer? How about if the script is supposed to reboot the computer running it at various points, and you want the script to pick right back up where it left off the next time it is run (post reboot)? Then this blog post is for you.

For ease of reference/testing, I am providing a sample script that was used in creation of this blog post. Please read through this blog post entirely if you plan to download and test it out, otherwise you might not understand something important.

Now you may be asking yourself why use an XML file to track a script’s progress versus a registry value or even a PowerShell workflow? Like in all things PowerShell there are multiple ways of doing the same things and sometimes you just have to pick one and go with it, but my reasons for this path were as follows:

  • I didn’t like the method of storing script data in registry keys as I don’t want to store a value in an existing key for fear of interfering with some other program or process. The alternative is creating a new unique key somewhere to store the value, but where and what the best name should be is a guessing game. Most importantly I don’t like messing with the registry if I don’t have to. 😊
  • While PowerShell workflows can restart a computer and pick up where it left off, workflows are more complicated than standard PowerShell scripting which makes it more challenging to hand a script over to someone not familiar with them. That and I was also concerned about having a workflow resume at the right location if something terminated unexpectedly.
  • Plus, an XML file can be used to track lots of other information, even in a hierarchical fashion, not just the current step a PowerShell script is at. It can also be quickly copied off and referenced for historical/tracking purposes.

In all honesty I can’t claim credit for coming up with the idea of tracking a script progress in an XML file. In fact, the first time I started using this technique was when my colleague Doug Blanchard transitioned a script to me that is uses an XML file to track its progress preparing a server for an Exchange install. Since that transition a couple of years ago, I have made some changes and enhancements to the process, and today I want to share with you how it all works.

At a high level, the are 3 components used for PowerShell to track and take action based on the script’s progress:

  1. An XML file with a value called ScriptStep in it, where the step number is recorded (starting at 1).
  2. A PowerShell Switch statement that uses numbers for the switches that correspond to the individua steps in the script.
  3. A labeled While loop construct in the PowerShell script that encapsulates the Switch statement.

Creating and Using an XML File in PowerShell

The first component needed is an XML file with the ScriptStep value. While the XML file can be generated outside the PowerShell script that needs it, it’s a good idea to just have the script create define the contents of the file so you have everything in one spot. To that point I give you the following example code:

 
# Define the path and name of the XML file.
$XMLFile = "C:\Temp\ScriptProgress.XML"

# Test to see if the XML file already exists, and if it doesn't then create it.
If (-not(Test-Path $XMLFile)) {
    Write-Host "No preexisting XML file found. Creating new one now."
    # NOTE the XML file creation using the @ method doesn't tolerate indenting, so it must fully left justified.
    # Also start the Script Step Value as 1 every time the XML file is created.
$XMLContent = @"
<Computer>
  <ScriptStep>1</ScriptStep>
  <OtherInformation></OtherInformation>
  <DateTime></DateTime>
  <ScriptVersion></ScriptVersion>
  <SystemChecks>
    <ServerName></ServerName>
    <OSCaption></OSCaption>
    <OSBuild></OSBuild>
  </SystemChecks>
</Computer>
"@
    # Check to make sure the folder that will contain the XML file exists, and if not create it.
    $XMLFolder = Split-Path $XMLFile -Parent
    If (-not(Test-Path $XMLFolder)) {
        New-Item -Path $XMLFolder -ItemType Directory -Force -Confirm:$False | Out-Null
    }

    # Save the blank template to the XML file and set NewXML to True in the script scope.
    $XMLContent | Out-File -FilePath $XMLFile -Encoding ASCII -Force | Out-Null
}

# Read in the contents of the XML file as an XML variable.
[XML]$XMLData = Get-Content $XMLFile

Take note that the formatting of an XML file inside a PowerShell script is very particular in that it must always be fully justified left, and each sub-value must be indented 2 spaces below the parent value’s indentation level. Furthermore you can only have one root level value, in our example “Computer”, and everything else must be under that. So further in the example above, ServerName is a sub-value to SystemChecks, and can be represented as $XMLData.Computer.SystemChecks.ServerName later in PowerShell.

Creating a Switch Statement that Leverages ScriptStep

Now that the XML file is created and has a value for script step, which is defaulted to step 1 in the example above, we need to tell PowerShell how to reference it in regards to which step to execute. Here is some additional sample code showing just that:

 
# Begin processing this script based on XML Script Step Value.
Switch ($XMLData.Computer.ScriptStep) {
    1 {
        # Collect system information.
        Write-Host "1 - Collecting system information."
        # Add more code here.
        $XMLData.Computer.ScriptStep = "2"
        $XMLData.Save($XMLFile)
    }
    2 {
        # Perform system changes.
        Write-Host "2 - Performing system changes."
        # Add more code here.
        $XMLData.Computer.ScriptStep = "3"
        $XMLData.Save($XMLFile)
        # Reboot if needed.
    }
    3 {
        # Configure disks.
        Write-Host "3 - Configuring disks."
        # Add more code here.
        $XMLData.Computer.ScriptStep = "4"
        $XMLData.Save($XMLFile)
    }
    4 {
        # Install Software: .NET Framework, HotFixes, etc.
        Write-Host "4 - Installing required software."
        # Add more code here.
        # Reboot if needed. NOTE: not updating the XML file will cause this step to be repeated which is sometimes desired with hot fixes that have dependencies of one needing to go on before another.
        $XMLData.Computer.ScriptStep = "5"
        $XMLData.Save($XMLFile)
        # Reboot if needed after the XML file update.
    }

    # Add as many more numerical steps as needed here...

    Default {
        # This Default switch should never be accessed but is set start back at the first step just in case.
        Write-Warning "We should not be here...Danger Will Robinson...Danger."
        $XMLData.Computer.ScriptStep = "1"
        $XMLData.Save($XMLFile)
    }
}

As you can see the Switch statement is pulling the current numerical value from the in-memory XML ScriptStep value, and then executing the corresponding numbered switch section of code. Once that switch section is complete, the number in the in-memory XML ScriptStep value is updated to the next desired step and the XML changes are committed to disk. Normally these numbers would be sequential, but you may want to jump around for whatever reason. Also you can reboot in mid-step with the intention of having the step repeat as might be needed for updates that have dependencies on one another being installed first.
NOTE: In this code, the Default switch is used as a catch call as it should never be used unless some unexpected or blank value made it into ScriptStep.

Creating a While Loop that Repeats the Switch Statement

Normally a PowerShell Switch statement executes just once, checking each defined switch value against the value used in the main Switch statement, and executing any matches it fines. But what if you want the Switch statement to keep repeating until it is no longer needed? To that end you can wrap the Switch statement in a While loop that keeps repeating it until the While loop is told to stop. Here is an example of While loop wrapping a Switch statement:

 
# Set the StayInSwitch variable to true so the switch will always be run through until the last step called where it is set to false.
$StayInSwitch = $True
:MainSwitch While ($StayInSwitch) {
    # Begin processing this script based on XML Script Step Value.
    Switch ($XMLData.Computer.ScriptStep) {
        1 {
            # Collect system information.
            Write-Host "Collecting system information."
            # Add more code here.
            $XMLData.Computer.ScriptStep = "2"
            $XMLData.Save($XMLFile)
            Continue MainSwitch
        }

        # Add as many more numerical steps as needed here...

        8 {
            # Create a summary report.
            Write-Host "Creating a summary report."
            # Add more code here.
            $XMLData.Save($XMLFile)
            $StayInSwitch = $False
        }
        Default {
            # This Default switch should never be accessed but is set start back at the first step just in case.
            Write-Host "We should not be here...Danger Will Robinson...Danger."
            $XMLData.Computer.ScriptStep = "1"
            $XMLData.Save($XMLFile)
            Continue MainSwitch
        }
    }
}

As you can see we set the value $StayInSwitch to $True so that the While loop will keep executing the Switch statement until $StayInSwitch is set to $False which is done in the last step. You could change this value to $False in other steps if say some condition is or isn't met and a result you don’t want the rest of the steps to execute.

Also, the While loop has an added label called "MainSwitch"  which is used by the Continue command at the end of each step. Without the use of "Continue MainSwitch", the remaining steps in the Switch statement would be evaluated against the original value of ScriptStep from when the Switch was initiated (even if it was updated to a new number after the Switch started). For example, let’s say you have steps/switches 1-8 and entered the Switch statement with the ScriptStep value of 1. The switch #1 would execute because it matched the ScriptStep value, and after it was done the Switch statement would evaluate steps/switches 2-8 against the original ScriptStep value of 1 even you changed ScriptStep to another number while in inside switch #1. That may be confusing, but Switches are designed to work with a variety of values, not just numbers, and the value passed into the Switch statement might match multiple switches so it tries to compare them all (even after it matches one).

Using the Continue command at the end of a switch ensures the Switch statement doesn’t waste any time comparing other switches against the original value, and instead relaunches the Switch statement (through the re-initialization of the While loop) with the new ScriptStep value. Even if Switch statements did work the way I explained above, re-initializing the While loop and subsequently the Switch statement gives you the option to jump around steps as needed such as re-calling an earlier switch or jumping ahead to a much later switch.

Bonus Feature: Starting a Script at a Specific Step

An added bonus of this approach is that you can execute a script and tell it to start at a specific step by using a script parameter. You might do this in the case where you want to re-run one specific step because maybe it didn’t execute correctly the first time, or you just want to run a few steps instead of all of them in order. Either way this sample code will add that ability:

 
# Define the StartAtStep script parameter like any other script parameter.
[CmdletBinding()]
Param (
    [Parameter(Mandatory = $False)]
    [Int]$StartAtStep
)

# Load the XML file here.

# Check to see if the StartAtStep parameter was used and then override whatever was in the XML file as the step to start at.
If ($StartAtStep) {
    $XMLData.Computer.ScriptStep = [String]$StartAtStep
    $XMLData.Save($XMLFile)
}

# Execute the While loop with embedded Switch statement here.

Closing Thoughts

So there you go, you can now add individual step tracking to your PowerShell scripts so that you can start them at any point you want, always know where they left off (by checking the XML file contents), and even have them pick up with where they left off the next time they are executed. Here is an example of the sample script provided above in action:

If you combine this functionality with a PowerShell script configuring Windows auto-logon and automatic re-execution of a script, you can have a pretty powerful script that runs through multiple steps including reboots in a completely automated fashion.

Please feel free to leave me comments here if you wish, I promise I will try to respond to each in kind.

Thanks!
Dan Sheehan
Senior Premier Field Engineer

Comments

  • Anonymous
    June 11, 2018
    Awesome! Thanks for this. It is exactly what I need.
    • Anonymous
      June 11, 2018
      Thanks for the comment, it's nice to know one's efforts to share are appreciated. :)
  • Anonymous
    June 24, 2018
    Nice. My long-time Exchange 2013/2016 Install script uses similar state machine logics.
    • Anonymous
      June 24, 2018
      Thanks for the comment. No doubt this is a method that is used in various forms in the wild. Even I inherited the base methodology before kicking it up a new notches.
  • Anonymous
    June 24, 2018
    I'm probably missing something here, but why would you create the XML file this way instead of using Export-Clixml/Import-Clixml?
    • Anonymous
      June 25, 2018
      The comment has been removed