Debugging PowerShell DSC Class Resources

Update:

I neglected to mention that the DSC resource debugging enabling Cmdlet, Enable-DscDebug, is available only in the Technical Preview 5 release and this was not available when this blog was first published. My sincerest apologies for that. Fortunately this release is now available at:

System Center 2016 Preview and Windows Server 2016 Technical Preview 5 released

 

Traditionally PowerShell script debugging has been limited to script running inside the PowerShell console or ISE (Integrated Script Environment), which means you are limited to debugging script that you can see and is running interactively. This is great most of the time as it allows you to experiment with and debug PowerShell script by setting breakpoints and stepping through the script code. But PowerShell is more than just a command line shell that supports scripting. It is also a robust management automation platform where scripts can run concurrently and non-interactively. How do you debug these scripts? A perfect example of this is PowerShell DSC (Desired State Configuration). DSC lets you create special PowerShell script functions or classes for the purpose of configuring remote machines (https://blogs.technet.microsoft.com/privatecloud/2013/08/30/introducing-powershell-desired-state-configuration-dsc/).These resource scripts run on remote target machines in a special host process. How on earth do you debug these scripts? Sure you can begin by adding a lot of verbose messaging to your scripts. But ideally you would like to debug a live session by setting breakpoints, stepping through the code and viewing variables just like you can with script running in the console or ISE.

Fortunately PowerShell V5 has new debugging features that allow you to do just this. In this article I will briefly discuss the new PowerShell 5.0 cmdlets that help you attach to arbitrary local PowerShell hosting processes and debug running PowerShell script. I’ll then talk about how PowerShell DSC uses these new PowerShell features for DSC resource debugging on a target computer. Along the way I’ll point out current limitations when debugging live DSC resource scripts, and ways DSC resource script debugging can be improved in the future.

PowerShell V5 Advanced Debugging Cmdlets

As you might expect debugging arbitrary running scripts on a local or remote machine is a bit more complex than simply setting breakpoints in a file you have open in the ISE and then hitting the F5 key. There are two new sets of cmdlets to help with this. The first involve using PowerShell runspace objects to find and debug running scripts in a session. The second deal with attaching a console or ISE session to a separate local process that hosts PowerShell. Combining these together gives you the ability to debug any scripts on a local or remote machine.

What are Runspaces?

First you need to understand PowerShell runspaces since advanced debugging is necessarily built around these.

https://blogs.technet.microsoft.com/heyscriptingguy/2015/11/26/beginning-use-of-powershell-runspaces-part-1/

Runspaces are PowerShell abstractions that define the context in which PowerShell script and commands run. Normally you don’t care about runspaces because when you run script inside the console or ISE a “default” runspace is created for you. You don’t need to be aware of the runspace, it just all works. But if you want to run other background scripts in the session, you can do this too by creating more runspaces. Each background script runs in its own runspace while allowing you to continue to use the session console. This is exactly how PowerShell jobs work. You can think of runspaces as containing all of the state needed to run a script, such as variables, functions, loaded modules, etc. Since scripts run within a runspace, debugging an arbitrary running script is synonymous to debugging its runspace.

New Runspace Cmdlets

There are five new cmdlets that help you discover and debug runspaces.

Get-Runspace

This cmdlet is for discovery and lists all runspaces in the session. Running this from the console or ISE will normally return just one (default) runspace.

Debug-Runspace

This cmdlet is run from the console or ISE and performs a “debugger attach” operation to the specified runspace. It can’t be used on the default runspace (you will get an error if you try) because that already has the console debugger associated with it. But for other runspaces it lets you attach the existing console or ISE debugger to that non-default runspace. The debugger remains attached until you type Ctrl+C at the keyboard to get back the command line prompt. When the debugger is attached the debugger is put in “break all” mode and if the runspace happens to be running script then the debugger will automatically break at the next script execution point. Otherwise it will wait until script begins running and then break at the first available execution point.

Enable-RunspaceDebug

This cmdlet enables the runspace for debugging. This means that it will wait for a debugger to be attached when a breakpoint is hit. It also has a -BreakAll switch parameter that tells the runspace to immediately stop in the debugger when running a new script. Be careful with this as script stopped and waiting for a debugger attach can appear to be hung, when in fact it is just waiting for a debugger attach. The runspace Availability property tells you if it is waiting for a debugger with a value of “InBreakpoint”.

Disable-RunspaceDebug

This cmdlet disables the runspace for debugging by reverting it back to normal operation. It also releases the debugger if it is stopped at a breakpoint.

Get-RunspaceDebug

This cmdlet returns the current debug setting of the runspace

Attach to Host Process Cmdlets

PowerShell now allows you to attach to another local process that is hosting PowerShell. By doing this you can see what runspaces that host process has and whether it is running any script. And of course you can debug any script running in that runspace. Note that you can attach to an arbitrary hosting process only if you have Administrator privileges on that machine. Otherwise you are restricted to host processes that were created with your credentials. There are three cmdlets to support these operations.

Get-PSHostProcessInfo

This cmdlet returns all local processes that are hosting PowerShell and that you can attach to.

Enter-PSHostProcess

This cmdlet lets you attach to the process from the console or ISE and interact in a PowerShell session in that process. This means you can run commands or script in that process including finding and debugging runspaces. This cmdlet is very similar to the existing Enter-PSSession cmdlet except that you are interacting with an existing local host process rather than a remote machine.

Exit-PSHostProcess

This cmdlet exits the interactive session with the host process and severs the connection.

Debugging PowerShell DSC Resource Scripts

Congratulations! You are now armed with the knowledge needed to use and understand PowerShell DSC resource debugging. I could have skipped the first part and started here but I thought it worthwhile to first explain (admittedly briefly) the new PowerShell debugging features\cmdlets that this is based on.

PowerShell DSC runs resource scripts on a target machine in the LCM (Local Configuration Manager) that is hosted in a wmiprvse.exe process. So from the above discussion you can immediately see how this will work. The DSC resource script is run on the target machine, stops in the debugger and waits for a debugger attach. We use the cmdlets discussed above to attach to the LCM host process and then debug the runspace running the resource script.

To do this the script is run in a special LCM debug mode. Currently this debug mode only supports break all functionality where the LCM running script is stopped at the first execution point and waits for a debugger attach. Hopefully in the future the LCM debug mode will support script source file line breakpoints so that the script is stopped right where you want. There is another way to get the script to stop where you want it to, but it involves modifying the script source. See the Wait-Debugger section below.

DSC Debug Mode Cmdlets

Enable-DSCDebug

This cmdlet sets the LCM on the machine to debug mode. It currently only supports -BreakAll. The LCM BreakAll mode causes any resource script execution to stop immediately and wait for a debugger attach to be attached.

Disable-DSCDebug

This cmdlet reverts the LCM back to normal operation.

DSC resource script debugging steps

  1. Set the LCM on the target machine to debug break all mode (Enable-DSCDebug -BreakAll)
  2. Run the DSC configuration and wait for the script to stop in the debugger (you will receive messages telling when this happens)
  3. Follow the message instructions to attach the debugger to the runspace running the script
  4. Run “step into” debugger command until you get to your resource method (Test, Get, Set)
  5. Set line breakpoints as needed and debug the script
  6. When finished, detach the debugger
  7. Set the LCM back to non-debug mode (Disable-DSCDebug)

Example

This example illustrates how to debug DSC class resource scripts. I have created a simple DSC class resource named ‘FileResource’ that copies or removes a file during configuration. A DSC resource class must have at least three methods (Test, Set, and Get) but can have other helper methods, and in this example I have a CopyFile helper method that performs the file copy.

Simple resource class for test

##
## Test Class based resource
##

enum Ensure
{
    Absent
    Present
}

[DscResource()]
class FileResource
{
    [DscProperty(Key)]
    [string] $Path

    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Mandatory)]
    [string] $SourcePath

    [DscProperty(NotConfigurable)]
    [Nullable[datetime]] $CreationTime


    [void] Set()
    {
        $fileExists = $this.TestFilePath($this.Path)
        if ($this.ensure -eq [Ensure]::Present)
        {
            if (! $fileExists)
            {
               $this.CopyFile()
            }
        }
        else
        {
            if ($fileExists)
            {
                Write-Verbose -Message "Deleting the file $($this.Path)"
                Remove-Item -LiteralPath $this.Path -Force
            }
        }
    }

    [bool] Test()
    {
        $present = Test-Path -LiteralPath $this.Path

        if ($this.Ensure -eq [Ensure]::Present)
        {
            return $present
        }
        else
        {
            return (! $present)
        }
    }

    [FileResource] Get()
    {
        $present = Test-Path -Path $this.Path

        if ($present)
        {
            $file = Get-ChildItem -LiteralPath $this.Path
            $this.CreationTime = $file.CreationTime
            $this.Ensure = [Ensure]::Present
        }
        else
        {
            $this.CreationTime = $null
            $this.Ensure = [Ensure]::Absent
        }

        return $this
    }

    [void] CopyFile()
    {
        if (! (Test-Path -LiteralPath $this.SourcePath))
        {
            throw "SourcePath $($this.SourcePath) is not found."
        }

        if (Test-Path -LiteralPath $this.Path -PathType Container)
        {
            throw "Path $($this.Path) is a directory path"
        }

        Write-Verbose "Copying $($this.SourcePath) to $($this.Path)"

        Copy-Item -LiteralPath $this.SourcePath -Destination $this.Path -Force
    }
}

Configuration file

##
## Configuration for DSC test class
##

Configuration DSCTestClass
{
    Import-DSCResource -module DSCClassResource
    FileResource file
    {
        Path = "C:\temp\test\test.txt"
        SourcePath = "c:\temp\test.txt"
        Ensure = "Present"
    } 
}

DSCTestClass

The DSC resource is already on the target machine so I will start by enabling DSC resource script debugging and starting the configuration.

Enable-DscDebug -BreakAll
Start-DscConfiguration .\temp\DSCTestClass -wait force

ScreenImg1

Notice that I started the DSC configuration using the -wait parameter to make it run interactively so that I can see the output in the ISE console window. We see a warning message that tells us the resource script is stopped and waiting for a debugger to be attached. It then provides us with the command lines needed to connect to the computer, attach to the process, and debug a specific runspace. The first command (Enter-PSSession) is optional and not needed when debugging directly on the local machine. In this example I am working in a remote desktop to the target machine so I can skip this first command. I then open a new ISE window and run the next two provided commands in the ISE to get the debugging session. I like using the ISE for script file debugging because it provides a much nicer debugging experience when compared to the PowerShell console.

Keep in mind that you need to have Administrator permissions on the target machine in order to debug DSC scripts.

Enter-PSHostProcess -Id 944 -AppDomainName DscPsPluginWkr_AppDomain
Debug-Runspace -Id 4

ScreenImg2

We now have a live debugging session! Notice how the command line prompt changes after running each command. After the Enter-PSHostProcess command you see the prompt indicate that we have attached to process 944, which is the wmiprvse.exe process hosting the LCM. Then the Debug-Runspace cmdlet attaches the debugger to the runspace with Id 4 and the prompt shows [DBG] to indicate you are in a debugging session, [Process:944] because you are attached to that host process, and [Runspace4] to indicate the runspace you are debugging.

Also there is no resource script source file window open in the ISE yet. This is because that resource script has not run yet and so the ISE debugger doesn’t know about it. Instead we see in the ISE console the launching script that the LCM uses to run the first resource method ($global:DSCClassObject.Test()). We simply keep running the ‘step into’ debug command (F11 key) until the class method is run, and at this point the ISE opens the source script file.

ScreenImg3

From the above screen shot you can see that after pressing F11 three times the script calling $global:DSCClassObject.Test() method completed at which point the ISE found the source file, opened it in the debugger, and displayed the current execution point of the Test class method. Now we can step through the resource class code as it executes in the LCM by using the F10 and F11 keys. We can also set line breakpoints using the F9 key. And we can view variables. Awesome. I saw that the $present variable was false and since my configuration requires the file to be present I expect the Set method to be called next. I then pressed F5 key to let execution continue.

ScreenImg4

As expected we stopped next in the Set class method. Here I want to debug the CopyFile helper class method that does all of the work and so set a line breakpoint where it is called.

Important Note!

Once again I needed to press the F11 key three times to step through the script stub that runs the Set Method. This is because the LCM uses the same script stub to run each class method and you need to step through this code to get to the class method. Remember that break all debug mode stops at the first execution point. It is a blunt instrument but works. But additionally the LCM resets the runspace each time it runs a class method and, unfortunately, this also means all breakpoints are removed. So you cannot expect breakpoints to persist between each class method invocation and so you will need to set line breakpoints each time.

Wait-Debugger

The Enable-DscDebug -BreakAll debugging mode is useful but can be a bit difficult to use, especially if you need to debug a specific method/function in complex resource code. At some point I hope that the LCM will support line breakpoints. But in the meantime there is one other technique you can use to help this situation involving the new Wait-Debugger cmdlet. What this cmdlet does is tell the PowerShell script execution engine to stop at the script execution point immediately after the Wait-Debugger cmdlet, and wait for a debugger to be attached. Basically it does the same thing as Enable-RunspaceDebug -BreakAll but at a specific point in the script.

The downside to using Wait-Debugger is that you need to modify the source script. But if you are tracking down a difficult to find bug on a specific machine that you control, you can (temporarily) insert the Wait-Debugger cmdlet in the script code, attach the ISE debugger and do live script debugging at that point.

CAUTION: Make sure you remove the Wait-Debugger lines after you are done! Running script can appear hung when it is stopped at a Wait-Debugger if you forgot you added the cmdlet to your script. You certainly don’t want to have this cmdlet in production script!

ScreenImg5

Here I leave the LCM in BreakAll debug mode (Enable-DscDebug -BreakAll) and then I just keep pressing the F5 run key until the ISE opens the source file where I inserted Wait-Debugger command. Notice that the current execution point is the line immediately after the Wait-Debugger. Now I have a live debug session exactly where I want!

Conclusion

DSC configuration is really great but also pretty complex. You create resource scripts to define configuration state but these scripts run on remote machines and in some arbitrary hosting process. Sometimes you require a live debugging session to ensure your script is working correctly. Now, with DSC debug mode and the new PowerShell advanced debugging features, you can debug your resource scripts while running in a live session on a target computer. This live debugging session is within the LCM process running in the LocalSystem context (Administration credentials are required), which is a great improvement over debugging your resource scripts on your local development machine.

Paul Higinbotham
Senior Software Engineer
Windows PowerShell Team