PowerShell in Depth, Second Edition (2015)
Part 3. PowerShell scripting and automation
Chapter 31. Debugging tools and techniques
This chapter covers
· Understanding the debugging methodology
· Working with debugging cmdlets
· Using breakpoints
· Remote debugging
· Debugging workflows
Debugging is a difficult topic in any kind of computer system, and PowerShell is no exception. PowerShell provides decent tools for debugging, but those tools don’t make debugging magically easier. The trick with debugging, it turns out, is having an expectation of how things are supposed to work so that you can spot the place where they stop doing so.
For example, let’s say your car won’t start. Do you have any idea why? Do you know what’s supposed to be happening when you turn the key? If not, then you can’t debug the problem. If you know that the battery needs to be charged, your foot has to be on the brake, the starter motor has to be wired up and functional, and so forth, you can start debugging by testing each of those elements individually.
All PowerShell can do is give you tools, things that you can use to check the current state of the shell, a script, or something else. PowerShell can’t tell you whether or not the current state is correct. It’s like having an electrical meter: If you don’t know where to put it, how to read the output, and what the output is supposed to be and why, the tool isn’t all that helpful.
Computers are no different. If Active Directory replication isn’t working, you can’t troubleshoot—or debug—that problem unless you know how all the little components of replication are supposed to work and how you can check them to see if they’re doing so. From that perspective, PowerShell doesn’t make debugging any more difficult. It just tends to highlight the fact that we’ve been so separated from Windows’ internals by its GUI for so long that we often don’t know how the product is working under the hood. PowerShell gives you great tools for checking on things, but if you don’t know what to check, and don’t know what it should look like when you do check, you can’t troubleshoot it.
31.1. Debugging: all about expectations
Let’s be very clear that, when it comes to a PowerShell script, you can’t debug it unless you think you know what it’s doing. If you’re staring at a screenful of commands and haven’t the slightest idea what any of it’s supposed to do, you’re not going to be able to debug it. If you’ve pasted some code from someone’s internet blog and don’t know what the code is supposed to do, you won’t be able to fix it if it doesn’t do what you want. So the starting point for any debugging process is to clearly document what your expectations are. In the process of doing so, you’ll quickly identify where your knowledge is incomplete, and you can work to make it more complete.
To that end, let’s start with the following listing, a script that was donated to us by a colleague. We’ll use it to walk you through how you can go about this identifying-expectations process.
Listing 31.1. A script to debug
function Get-DiskInfo {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$computerName,
[Parameter(Mandatory=$True)]
[ValidateRange(10,90)]
[int]$threshold
)
BEGIN {}
PROCESS {
foreach ($computer in $computername) {
$params = @{computername=$computer
filter="drivetype='fixed'"
class='win32_logicaldisk'}
$disks = Get-WmiObject @params
foreach ($disk in $disks) {
$danger = $True
if ($disk.freespace / $disk.capacity * 100 -le $threshold) {
$danger = $False
}
$props = @{ComputerName=$computer
Size=$disk.capacity / 1GB -as [int]
Free = $disk.freespace / 1GB -as [int]
Danger=$danger}
$obj = New-Object –TypeName PSObject –Property $props
Write-Output $obj
}
}
}
END {}
}
Note
The script in listing 31.1 doesn’t work as is. We know that. It’s a chapter on debugging. You’re going to fix it as you go.
Don’t even try to run this script; we’ll start by walking you through each section and documenting what we think it should do. We follow this exact process all the time, although with experience you’ll start doing bits of it in your head and moving more quickly. For right now, let’s take it slow. Have a pencil and blank sheet of paper ready, too—you’ll need that. First up:
function Get-DiskInfo {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[string[]]$computerName,
[Parameter(Mandatory=$True)]
[ValidateRange(10,90)]
[int]$threshold
)
BEGIN {}
This section appears to be defining two parameters. They’re both listed as mandatory, so you should expect to run Get-DiskInfo with both a –computerName and a –threshold parameter. Notice that –computerName accepts an array of names. At the moment you don’t know whether or not that’s significant. The latter appears to accept values in the range from 10 to 90. If you don’t know what every single piece of information here means to PowerShell, you should look it up; it’s all covered elsewhere in this book.
This section of the script ends with a BEGIN block, which is empty, so record that fact but otherwise ignore it. Here’s the next hunk of code:
PROCESS {
foreach ($computer in $computername) {
$params = @{computername=$computer
filter="drivetype='fixed'"
class='win32_logicaldisk'}
$disks = Get-WmiObject @params
This section is a PROCESS block. We expect that, when this function is given pipeline input, the PROCESS block will run once for each piped-in item. If there’s no pipeline input, then this entire PROCESS block will execute once. The ForEach block will enumerate through the contents of the$computerName parameter, taking one value at a time and putting it into the $computer variable. So if you ran Get-DiskInfo –comp SERVER1,SERVER2 –thresh 20, you’d expect the $computer variable to initially contain SERVER1. Take that blank sheet of paper and your pencil, and write that down:
$computer = SERVER1
$threshold = 20
Next, this section is creating a hash table named $params. It looks like it’ll contain three values, and those are being fed as parameters (via the technique called splatting) to the Get-WmiObject command. Two of those parameters, class and filter, are fixed, whereas the other,computerName, is variable. The –computerName parameter is being given the contents of the $computer variable. Well, we think we know what that variable contains, right? Assuming SERVER1 is a legitimate computer name on the network, let’s just try running that command (ifSERVER1 won’t work on your network, substitute localhost, $env:COMPUTERNAME, or another valid computer name).
Tip
The techniques in this chapter take advantage of the fact that the same commands can be run interactively as in your script. Whenever you’re in doubt about some code, try running it interactively to see what happens—it can save you lots of time.
You’ll expand Get-WmiObject @params and test this part of the function by hand-coding the values and parameters:
PS C:\> get-wmiobject -class win32_logicaldisk -filter "drivetype=
'fixed'" -computername localhost
Get-WmiObject : Invalid query
At line:1 char:14
+ get-wmiobject <<<< -class win32_logicaldisk -filter "drivetype='fi
xed'" -computername localhost
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject],
ManagementException
+ FullyQualifiedErrorId : GetWMIManagementException,Microsoft.Po
werShell.Commands.GetWmiObjectCommand
Well, there’s a problem. Your expectation was that this would do something other than produce an error. But you got an error. So you have one of two things going on:
· You don’t know what you’re doing.
· The command is wrong.
Far too often, people tend to assume that the first possibility is true, and we beg you not to do that. Prove to yourself that you’re wrong—don’t just assume it. For example, in this case, your command had three parameters. Take one off—remove –computerName and try again:
PS C:\> get-wmiobject -class win32_logicaldisk -filter "drivetype=
'fixed'"
Get-WmiObject : Invalid query
At line:1 char:14
+ get-wmiobject <<<< -class win32_logicaldisk -filter "drivetype='fi
xed'"
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject],
ManagementException
+ FullyQualifiedErrorId : GetWMIManagementException,Microsoft.Po
werShell.Commands.GetWmiObjectCommand
Same problem. Okay, fine, put –computerName back so that you’re changing only one thing at a time.
Be methodical
The most important point in the whole chapter is that when you’re making changes during the debugging process, make only one change at a time so you can correctly record the outcome of that change. If you make multiple changes, you won’t know which caused the change in output.
You’ll see many people flailing around making multiple changes at random in the hope that something will work. Don’t follow that pattern.
Be methodical. Make single changes and record what you’re doing and the outcome. You’ll fix the problem much quicker that way.
As a second step, remove –filter:
PS C:\> get-wmiobject -class win32_logicaldisk -computername localhost
DeviceID : A:
DriveType : 2
ProviderName :
FreeSpace :
Size :
VolumeName :
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 32439992320
Size : 42842714112
VolumeName :
DeviceID : D:
DriveType : 5
ProviderName :
FreeSpace :
Size :
VolumeName :
Wow, that worked! Great. It also contains some interesting information, if you take a moment to look at it. The DriveType property is numeric, but in your original command, the –filter parameter was trying to set DriveType='fixed'—possibly that’s the problem. Looking at this output, you can guess that 3 is the numeric type for a fixed disk (it’s the numeric type for the C: drive, and we all know that’s a fixed disk—see the documentation for the Win32_LogicalDisk class on MSDN at http://msdn.microsoft.com/en-us/library/aa394173%28v=vs.85%29.aspx), so try modifying the command:
PS C:\> get-wmiobject -class win32_logicaldisk -filter "drivetype='3'"
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 32439992320
Size : 42842714112
VolumeName :
Awesome! You’ve fixed one problem. Let’s go back and modify it in the original script, with the following listing showing the revision.
Listing 31.2. Fixing the first problem
The important thing in listing 31.2 is that you found something that didn’t work and that you didn’t understand; you didn’t just plow ahead and ignore it. You stopped and tried to understand it, and you ended up finding a problem in the script. That’s fixed, and so you can move on. You’re still on this section:
PROCESS {
foreach ($computer in $computername) {
$params = @{computername=$computer
filter="drivetype='3'"
class='win32_logicaldisk'}
$disks = Get-WmiObject @params
This is just a fragment that won’t run on its own because it needs a close to the foreach loop. But you can take some of this and run it stand-alone. In the shell, you’ll create a variable named $computer and then paste in some of the previous code to see if it runs. The result is put into a variable named $disks, so you’ll check that variable’s contents when you’re finished to see what it did:
PS C:\> $computer = 'localhost'
PS C:\> $params = @{computername=$computer
>> filter="drivetype='3'"
>> class='win32_logicaldisk'}
>> $disks = Get-WmiObject @params
>>
PS C:\> $disks
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 32439992320
Size : 42842714112
VolumeName :
Now you know that bit of code works, and you know that it puts something into the $disks variable. That’s something you should note on your sheet of paper:
$computer = SERVER1
$threshold = 20
$disks = one disk, C:, drivetype=3, size and freespace have values
Now for the next chunk of code:
foreach ($disk in $disks) {
$danger = $True
if ($disk.freespace / $disk.capacity * 100 -le $threshold) {
$danger = $False
}
Here’s another ForEach loop. Now you know that in your test the $disks variable has only one thing in it, so you can just manually assign that to $disk and try running this code right in the shell. Even if $disks had multiple disks, all you’d need to do would be grab the first one just to run a little test. It looks like this:
PS C:\> $disk = $disks[0]
PS C:\> $danger = $True
PS C:\> if ($disk.freespace / $disk.capacity * 100 -le $threshold) {
>> $danger = $False
>> }
>>
Property 'freespace' cannot be found on this object. Make sure that i
t exists.
At line:1 char:19
+ if ($disk. <<<< freespace / $disk.capacity * 100 -le $thres
hold) {
+ CategoryInfo : InvalidOperation: (.:OperatorToken) []
, RuntimeException
+ FullyQualifiedErrorId : PropertyNotFoundStrict
Whoa, that’s not good. The error is saying that there’s no Freespace property. Okay, send that object to Get-Member and see what’s happening:
PS C:\> $disk | get-member
Get-Member : No object has been specified to the get-member cmdlet.
At line:1 char:11
+ $disk | gm <<<<
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidO
perationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShe
ll.Commands.GetMemberCommand
Looks like your $disk variable didn’t get populated. Okay, try with the original $disks variable; that should contain something:
PS C:\> $disks | get-member
TypeName: System.Management.ManagementObject#root\cimv2\Win32_L...
Name MemberType Definition
---- ---------- ----------
Chkdsk Method System.Management.Manage...
Reset Method System.Management.Manage...
SetPowerState Method System.Management.Manage...
Access Property System.UInt16 Access {ge...
...
That worked. So the problem is that because $disks contained only one thing, accessing $disks[0] probably didn’t get any data. So try to create $disk again, this time without using the array reference:
PS C:\> $disk = $disks
PS C:\> $danger = $True
PS C:\> if ($disk.freespace / $disk.capacity * 100 -le $thresh
old) {
>> $danger = $False
>> }
>>
Property 'capacity' cannot be found on this object. Make sure that it
exists.
At line:1 char:37
+ if ($disk.freespace / $disk. <<<< capacity * 100 -le $thres
hold) {
+ CategoryInfo : InvalidOperation: (.:OperatorToken) []
, RuntimeException
+ FullyQualifiedErrorId : PropertyNotFoundStrict
Well, this is a different problem at least. This time it’s telling you there’s no capacity property. Let’s look back to the Get-Member output, and it’s right. There’s no capacity. There is, however, size. Modify the pasted-in command to try that instead:
PS C:\> $danger = $True
PS C:\> if ($disk.freespace / $disk.size * 100 -le $threshold) {
>> $danger = $False
>> }
>>
The variable '$threshold' cannot be retrieved because it has not been
set.
At line:1 char:62
+ if ($disk.freespace / $disk.size * 100 -le $threshold <<<<
) {
+ CategoryInfo : InvalidOperation: (threshold:Token) []
, RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
Well, you’re making progress. This time, the shell says it’s upset because $threshold doesn’t exist. That makes sense because you never created it. You know from your sheet of scratch paper that the variable should contain 20, so set that and try again:
PS C:\> $threshold = 20
PS C:\> $danger = $True
PS C:\> if ($disk.freespace / $disk.size * 100 -le $threshold) {
>> $danger = $False
>> }
>>
PS C:\> $danger
True
Okay, you didn’t get any errors this time, and the $danger variable contains something. You’re not sure it’s correct, though, and you know what? This is getting more and more complicated to do by hand. You’re trying to keep track of a lot of different values, and you could be introducing errors of your own. So it’s time to start using PowerShell to take a load off. First you’ll fix the capacity thing, so the next listing is your new script.
Listing 31.3. Revising the script to correct another bug
31.2. Write-Debug
What you’ve been doing all along is trying to get inside the script’s head, and PowerShell has some good tools for making that easier, starting with the Write-Debug cmdlet. Because your function uses [CmdletBinding()], the –Debug switch is added automatically, and it controls theWrite-Debug output.
Tip
It’s worth adding [CmdletBinding()] to all your scripts and functions so that you can take advantage of the debug, verbose, and other functionality you gain.
Let’s go through the script and add Write-Debug at key points. The cmdlet has two purposes: to output messages and let you know where the script is, and to give you a chance to pause the script, check things out, and resume. You still need to have an expectation of what the script should be doing, because all you’re going to be able to do is compare your expectations to reality—and wherever they differ is potentially a bug. The next listing shows where you add Write-Debug.
Listing 31.4. Adding Write-Debug statements
Now it’s time to run the script:
PS C:\> . ./Get-DiskInfo
You dot-source the script to load the function:
PS C:\> Get-DiskInfo –threshold 20 –computername localhost -debug
DEBUG: Started PROCESS block
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
This shows you exactly what Write-Debug does: You can see where it’s displayed the message, Started PROCESS block, and then paused. You’re okay to continue, so you answer “Y” for “Yes” and press Enter, and the script will resume running.
DEBUG: Computer name is localhost
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
Okay, “localhost” is what you expected the computer name to be, so let the script continue:
DEBUG: Got the disks
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):s
Getting the disk information is a big deal; as you’ll recall, you had problems with this earlier when you were testing manually. So enter S to suspend the script. This means the script is still running, but you can get a command-line prompt while still inside the script’s scope. The prompt, as you’ll see, is slightly different:
PS C:\>>> $disks
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 32439992320
Size : 42842714112
VolumeName :
PS C:\>>> exit
You just displayed the contents of $disks, and they were exactly what you expected. You ran Exit to get out of suspend mode and to let the script continue. Because it had been prompting you whether to proceed, you’ll return to that prompt:
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
You answer “Yes” again, and the script proceeds to the next Write-Debug statement:
DEBUG: Working on disk C:
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
After the drive the size is checked:
DEBUG: Size is 42842714112
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
And then the free space:
DEBUG: Free space is 32439992320
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):y
So you’ve confirmed the contents of the drive’s Size and FreeSpace properties, which you’d seen earlier anyway when looking at $disks. That all looks good—the drive appears to be around 75 percent empty, so it’s not in any danger. That should be confirmed by the next debug statement:
DEBUG: Danger setting is True
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):s
Hold up a minute. Why is the Danger setting True? That means the $Danger variable contains $True. You can see in your script where that was set, but you expected that the following math would set it to $False because the drive’s free space isn’t less than the threshold value you specified in –threshold. So you’re going to suspend the script again and do the math manually:
PS C:\>>> $disk.freespace / $disk.size * 100
75.7188077188456
Yeah, the drive is about 75 percent free, which is what you expected. So is 75 less than or equal to 20?
PS C:\>>> 75 -le 20
False
Did you spot the problem? You got the logic backward. You started by assuming that the drive’s free space is in danger by setting $danger to $True. Then, if the drive’s free space is less than the threshold value, which in this example isn’t the case, you set $danger to $False. Had to think about that one for a second, but the logic is twisted. So there’s no point in continuing. Exit suspend mode:
PS C:\>>> exit
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help
(default is "Y"):h
And answer H to halt the command. This delivers an error message, just letting you know you’re the reason things quit running:
Write-Debug : Command execution stopped because the user selected the
Halt option.
At C:\test.ps1:34 char:20
+ Write-Debug <<<< "Danger setting is $danger"
+ CategoryInfo : OperationStopped: (:) [Write-Debug], P
arentContainsErrorRecordException
+ FullyQualifiedErrorId : ActionPreferenceStop,Microsoft.PowerSh
ell.Commands.WriteDebugCommand
Now you need to modify the script, as shown in the following listing.
Listing 31.5. Fixing the logic error
You’re so confident that this is the right set of changes that you’re going to run this again without –Debug and see what happens:
Free Danger ComputerName Size
---- ------ ------------ ----
30 False localhost 40
Perfect! So you hopefully see the value of Write-Debug. With it, you were able to get some visual feedback on the script’s execution. You could also suspend and check things out, and eventually you figured out your logic flaw. When you took off the -Debug parameter, the script ran normally. You didn’t need to pull out all of the Write-Debug statements; they’re fine staying in there and will be suppressed until you need to debug the script again.
31.3. Breakpoints
In a way, what you’ve done with Write-Debug was to manually set a breakpoint, a place where your script pauses so that you can take stock and see if everything is running according to expectations. PowerShell v2 introduced another kind of breakpoint, which you can set ahead of time. You can set three kinds of breakpoints:
· A breakpoint that occurs when execution in a script reaches the current line or column. It’s similar to using Write-Debug, except that you don’t have to insert the Write-Debug statement into your script.
· A breakpoint that occurs when a specified variable is read, modified, or either. It can be set in the shell globally or can be tied to a specific script filename to just debug that script.
· A breakpoint that occurs when a specified command is run. It can be set in the shell globally or can be tied to a specific script filename to debug just that script.
When any breakpoint occurs, you wind up in the same suspend mode as you saw earlier when you used Write-Debug; run Exit to let the script resume execution from the breakpoint. In the PowerShell ISE, you can set the line-number style of a breakpoint by going to the line where you want it and pressing F9 (you can also select the Toggle Breakpoint option from the Debug menu). Pressing F9 again on a line where there’s a breakpoint set will clear the breakpoint. You can also use a set of cmdlets to manage breakpoints:
· Set-PSBreakpoint establishes a new breakpoint; use its parameters to specify the kind of breakpoint, command and variable names, line numbers, and so on.
· Get-PSBreakpoint retrieves breakpoints.
· Remove-PSBreakpoint removes breakpoints.
· Enable-PSBreakpoint and Disable-PSBreakpoint work against a breakpoint that you already created; they enable you to turn existing breakpoints on and off temporarily without removing them and having to re-create them.
Here’s a quick example of using the breakpoint cmdlets:
PS C:\> Set-PSBreakpoint -Line 25 -Script ./get-diskinfo.ps1
ID Script Line Command Variable Action
-- ------ ---- ------- -------- ------
0 get-diskinfo.ps1 25
PS C:\> . .\get-diskinfo.ps1
PS C:\> Get-DiskInfo -computerName localhost -threshold 20
Entering debug mode. Use h or ? for help.
Hit Line breakpoint on 'C:\scripts\get-diskinfo.ps1:25'
At C:\scripts\get-diskinfo.ps1:25 char:25
+ foreach ($disk in $disks) {
+ ~~~~~~
PS> $disks
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 165753094144
Size : 249951154176
VolumeName :
PS C:\> exit
ComputerName Danger Free Size
------------ ------ ---- ----
localhost True 154 233
You can also set breakpoints in the PowerShell ISE. They work much the same way. In the ISE select the line you want to “break” on and press F9. Or set the breakpoint via the Debug menu by selecting Toggle Breakpoint. You should get something like figure 31.1.
Figure 31.1. ISE breakpoints
Run your script from within the ISE. If your script contains a single function, as ours does in this example, you won’t see anything until you run the command. In the ISE command prompt, run the function and it’ll hit the breakpoint, just as it did in the console:
PS C:\> get-diskinfo -threshold 20 -computerName $env:computername
Hit Line breakpoint on 'C:\Users\...\listing31-5.ps1:25'
[DBG]: PS C:\>>
This nested debug prompt has its own set of commands. Type ? at the DBG prompt:
[DBG]: PS C:\>> ?
s, stepInto Single step (step into functions, scripts, etc.)
v, stepOver Step to next statement (step over functions, scripts,
etc.)
o, stepOut Step out of the current function, script, etc.
c, continue Continue operation
q, quit Stop operation and exit the debugger
k, Get-PSCallStack Display call stack
l, list List source code for the current script.
Use "list" to start from the current line, "list <m>"
to start from line <m>, and "list <m> <n>" to list <n>
lines starting from line <m>
<enter> Repeat last command if it was stepInto, stepOver or
list
?, h displays this help message.
You can use it much the way you used the prompt in the console. You can look at variables:
[DBG]: PS C:\>> $disks
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 165753094144
Size : 249951154176
VolumeName :
When you’re ready to continue, type C. The script will run until it hits another breakpoint or ends. If you wish to end the debug process, type Q at any DBG prompt.
Use the Debug menu to disable breakpoints or remove them altogether. Disabling them saves you from setting them again should the need to debug arise again.
31.4. Using Set-PSDebug
Another tool you might want to use is the Set-PSDebug cmdlet. You can use this cmdlet to debug not only scripts but also commands you run directly from the command prompt. The primary way to use it is to turn on tracing by using the –Trace parameter. This parameter accepts three different values:
PS C:\> help set-psdebug -parameter Trace
-Trace <Int32>
Specifies the trace level:
0 - Turn script tracing off
1 - Trace script lines as they are executed
2 - Trace script lines, variable assignments, function calls,
and scripts.
Required? false
Position? named
Default value
Accept pipeline input? false
Accept wildcard characters? False
Most of the time, you’ll want to use a value of 2:
PS C:\> set-psdebug -Trace 2
PS C:\> get-diskinfo -threshold 20 -compu $env:computername
DEBUG: 1+ >>>> get-diskinfo -threshold 20 -compu $env:computername
DEBUG: ! CALL function '<ScriptBlock>'
DEBUG: 13+ BEGIN >>>> {}
DEBUG: ! CALL function 'Get-DiskInfo<Begin>' (defined in file 'C:\u...
DEBUG: 13+ BEGIN { >>>> }
DEBUG: 14+ PROCESS >>>> {
DEBUG: ! CALL function 'Get-DiskInfo<Process>' (defined in file 'C:...
DEBUG: 15+ >>>> Write-Debug "Started PROCESS block"
DEBUG: 16+ foreach ($computer in >>>> $computername) {
DEBUG: ! SET $foreach = 'SERENITY'.
DEBUG: 16+ foreach ( >>>> $computer in $computername) {
DEBUG: ! SET $foreach = ''.
DEBUG: 46+ >>>> }
DEBUG: 47+ END >>>> {}
DEBUG: ! CALL function 'Get-DiskInfo<End>' (defined in file 'C:\use...
DEBUG: 47+ END { >>>> }
The command ran and you can sort of see what it did. What you need to do is also turn on stepping:
PS C:\> set-psdebug -step -trace 2
Now when you run the command you’ll get the same type of interactive debugging we showed you earlier:
PS C:\> get-diskinfo -threshold 20 -computer $env:computername
Continue with this operation?
1+ >>>> get-diskinfo -threshold 20 -computer $env:computername
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help
(default is "Y"):
DEBUG: 1+ >>>> get-diskinfo -threshold 20 -computer $env:computername
DEBUG: ! CALL function '<ScriptBlock>'
Continue with this operation?
13+ BEGIN >>>> {}
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help
(default is "Y"):
...
When you’ve finished debugging, you’ll need to turn off stepping and tracing:
PS C:\> set-psdebug -Off
We’re not sure the cmdlet adds anything in debugging your scripts than what we’ve already demonstrated, although there’s one other use that might be of value when it comes to writing your scripts. At the beginning of your script, insert this command:
set-psdebug -strict
This command will force PowerShell to throw an exception if you reference a variable before it has been defined, thus eliminating bugs, often from typos, from the very beginning. But be careful. Set-PSDebug will turn this on for all scopes. In other words, once your script ends, strict mode will still be enabled in your PowerShell session, so this might lead to headaches. The other approach is to use Set-StrictMode –version latest, which works the same as Set-PSDebug –Strict but only for the current scope. Once your script ends, everything goes back to normal.
31.5. Remote debugging
So far what we’ve shown you has been debugging on the local machine. In PowerShell v2 and v3, it wasn’t possible to use the PowerShell debugger against remote scripts—that is, you couldn’t run the debugger through a PowerShell remoting session. If you needed to debug a script that existed only on the remote machine, you’d have to copy it to your machine or access the remote machine through a Remote Desktop Protocol (RDP) console or similar mechanism.
This changes in PowerShell v4. You can create a remoting session to another machine and debug a script on that machine through the remoting session. You can debug scripts, functions, workflows (we’ll cover workflow debugging in the next section), commands, and expressions that are running in the PowerShell v4 console on remote machines.
Note
Notice the restriction inherent in that last sentence. Debugging remote PowerShell code is a console-only activity. You can’t do this through the ISE. The remote computer must be running PowerShell v4.
Debugging, as you’ve seen, is an interactive activity. You know from chapter 10 that you can use Enter-PSSession to either create a remoting session or use an existing session. You can also use Enter-PSSession to enable you to reconnect to a disconnected session that’s running a script on a remote computer. If the script hits a breakpoint, the debugger is started in your session. If the script is paused at a breakpoint, the debugger will be started as you enter the session.
What does this look like in action? To find out, save the contents of listing 31.4 as get-diskinfo.ps1. You can save the code on a remote computer, or if you don’t have a remote computer handy, you can simulate this by saving to the local disk. We’ve saved the code into a folder calledTestScripts.
Note
We chose listing 31.4 deliberately because we know that an error still exists.
Figure 31.2 shows creating the remoting session, changing the working directory, and running the test script.
Figure 31.2. Entering a PowerShell remoting session and running the script
The first command in figure 31.2 creates the session:
Enter-PSSession -ComputerName $env:COMPUTERNAME
We change the working folder:
cd C:\TestScripts
We load the function by dot-sourcing it and then we run it:
. .\get-diskinfo.ps1
Get-DiskInfo -computerName $env:COMPUTERNAME -threshold 20
You can see from the results that free space of 165 GB is being reported on a 232 GB disk. This isn’t below the 20 percent threshold, so we need to track the error. You learned in section 31.2 where the errors lie so that you can easily set the appropriate breakpoints. If you were starting the debug process from scratch, you wouldn’t have this luxury.
Figure 31.3 shows the breakpoints being added.
Figure 31.3. Setting breakpoints
You’ve used Set-PSBreakpoint to add breakpoints of the variables $disk, $props, and $danger:
Set-PSBreakpoint -Variable disk, props, danger -Script .\get-diskinfo.ps1
Note
When you set breakpoints on variables, use just the variable name, not the $ symbol.
Looking at the code in the script you’d want to see the following:
· The data associated with a disk, including the size and amount of free space
· The variable $danger being set to $True
· The variable $danger being set to $False if the criterion on free space is met
· The value of the $props variable before the output object is created
You can achieve this by running:
Set-PSBreakpoint -Variable disk, props, danger -Script .\get-diskinfo.ps1
Now it’s time to run the script and step through the debug process. Using the PowerShell remoting session you created from figures 31.2 and 31.3, start the script as you normally would:
[RSSURFACEPRO2]: PS C:\TestScripts> . .\get-diskinfo.ps1
[RSSURFACEPRO2]: PS C:\TestScripts> Get-DiskInfo
-computerName $env:COMPUTERNAME -threshold 20
Entering debug mode. Use h or ? for help.
Hit Variable breakpoint on 'C:\TestScripts\get-diskinfo.ps1:$disk'
(Write access)
At C:\TestScripts\get-diskinfo.ps1:22 char:16
+ foreach ($disk in $disks) {
+ ~
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>> $disk
DeviceID : C:
DriveType : 3
ProviderName :
FreeSpace : 177410433024
Size : 248951861248
VolumeName : Windows
The script is dot-sourced to load the function, and then the function is called with the local machine name passed to the –computerName parameter and a threshold of 20. The script will run until it hits the first breakpoint; at that point, you see the message Entering debug mode. The information tells you that you’ve hit a variable breakpoint (shown in bold in the previous code) and indicates the variable that caused the breakpoint. Notice the prompt change. During normal operations, it includes the computer name, which is standard for remote interactive sessions:
[RSSURFACEPRO2]: PS C:\TestScripts>
When your session enters debug mode, the prompt changes to this:
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>>
You get a visual indicator that you’re in debug mode. You can examine the value of the $disk variable and determine that the data is as expected. Type exit to leave debug mode and move to the next breakpoint:
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>> exit
Hit Variable breakpoint on 'C:\TestScripts\get-diskinfo.ps1:$danger'
(Write access)
At C:\TestScripts\get-diskinfo.ps1:26 char:9
+ $danger = $True
+ ~
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>> $danger
True
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>>
The second breakpoint is reached and you have the opportunity to examine the $danger variable. As expected, it’s set to $True. Type exit to proceed to the next breakpoint:
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>> exit
Hit Variable breakpoint on 'C:\TestScripts\get-diskinfo.ps1:$props'
(Write access)
At C:\TestScripts\get-diskinfo.ps1:31 char:9
+ $props = @{'ComputerName'=$computer;
+ ~
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>> $props
Name Value
---- -----
ComputerName RSSURFACEPRO2
Danger True
Free 165
Size 232
[RSSURFACEPRO2]: [DBG]: PS C:\TestScripts>>
This breakpoint is for the $props variable, and you can see that the disk has 165 GB free out of 232 GB. The $danger variable is set to $True but a simple code check shows that it shouldn’t be.
As you’ll recall, we expected to see four breakpoints. The third should have been when $danger was set to $False. That obviously didn’t happen, so you should concentrate on that area, which leads to the discovery of the logic error you saw earlier. Typing exit one last time allows the script to run to completion.
You can remove the breakpoints in bulk like this:
Get-PSBreakpoint | Remove-PSBreakpoint
You can then exit the remote session or use it for other purposes.
One of the big things in PowerShell v3 was workflows. In that version of PowerShell, you couldn’t use the standard debugging cmdlets against workflows. Now you can.
31.6. Debugging workflows
As you learned in chapter 23, PowerShell v4 lets you debug workflows in the console or the ISE. There are some limitations to debugging workflows:
· Although you can view workflow variables in the debugger, you can’t set workflow variables through the debugger.
· Tab completion isn’t available in the workflow debugger.
· You can only debug workflows that are running synchronously; you can’t debug workflows running as jobs.
· Debugging nested workflows isn’t supported.
To illustrate, let’s use the simple introductory workflow in listing 23.1. It’s repeated here for your convenience:
workflow Test-Workflow {
$a = 1
$a
$a++
$a
$b = $a + 2
$b
}
Test-Workflow
Save the workflow as testwf.ps1. You can then set a breakpoint:
Set-PSBreakpoint -Script .\testwf.ps1 -Line 6
This code sets a breakpoint on the line
$b = $a + 2
Now run the script:
PS C:\> .\testwf.ps1
1
2
Entering debug mode. Use h or ? for help.
Hit Line breakpoint on 'C:\TestScripts\testwf.ps1:6'
At C:\TestScripts\testwf.ps1:6 char:10
+ $b = $a + 2
+ ~~~~~~
[WFDBG:localhost]: PS C:\TestScripts>>
The first thing to notice is the way the prompt changes to indicate that a workflow ID is being debugged. Once you’re in debug mode, you can investigate the value of variables, as you’ve seen earlier:
[WFDBG:localhost]: PS C:\TestScripts>> $a
2
[WFDBG:localhost]: PS C:\TestScripts>> $b
[WFDBG:localhost]: PS C:\TestScripts>>
$a has a value of 2, as you’d expect. $b doesn’t have a value because entering debug mode through a breakpoint on a line positions you at the start of the line in question that’s before that line is executed. You can run that one line of the script by typing s at the prompt:
[WFDBG:localhost]: PS C:\TestScripts>> s
At C:\TestScripts\testwf.ps1:7 char:5
+ $b
+ ~~
[WFDBG:localhost]: PS C:\TestScripts>> $b
4
[WFDBG:localhost]: PS C:\TestScripts>>
The command s is a shortcut for Step-Into. You’ve instructed the debugger to execute the next statement and stop. Other commands are available to skip functions, continue to the end of the script, and list the part of the script that’s executing. These commands are described in the help file about_debuggers.
In the current example, because the line of code that sets that value has been executed, you can now test the value of $b. You can continue to step through the code or exit debug mode and allow the workflow to complete.
Note
You can also debug the workflow when it’s executing on a remote computer. Set the line breakpoints and then invoke the workflow; you can specify remote computers with the –PSComputername parameter.
Unfortunately, it appears that you can set breakpoints only on line numbers (using Set-PSBreakpoint or the Debug menu in the PowerShell ISE) and not on variables or commands in the workflow.
31.7. Debugging in third-party editors
If you’re using a third-party scripting editor, you should be able to use the debugging cmdlets and breakpoints we’ve shown you in this chapter. In fact, most likely the editor will use these tools to provide a debugging feature for their product. Naturally we can’t cover how every editor handles debugging, so you’ll have to check product documentation.
31.8. Summary
Debugging can be tricky, but PowerShell provides straightforward tools that let you quickly compare your expectations to what a script is doing. The tricky part involves coming up with those expectations: If you don’t have an idea of what a script should be doing, then debugging it is almost impossible, no matter what tools you have. Finally, if you can prevent bugs from happening in the first place with cmdlets like Set-StrictMode, you’ll reduce the amount of time you need to spend debugging. We recommend reading the help file about_Debuggers.