creepybugOne of the more tantalizing potentials in StudioShell is using it with the Visual Studio debugger.  This post will describe some of the features that are currently available. 

The debugger features are accessible at the path dte:/debugger.  A quick call to get-childitems shows how the functionality is broken down:

§ DTE:\debugger>ls Container: PSDTE::dte:\debugger Name ---------- ---- d+ < Breakpoints d < DebuggedProcesses d < LocalProcesses

There are two major subcategories of debugger features exposed through StudioShell: breakpoints and processes.

Breakpoints

StudioShell lets you add, remove, and modify breakpoints from script using the standard item cmdlets.  Here is a simple example that adds a breakpoint to line 25 of Program.cs (the ` line-continuation is used for readability):

new-item -path dte:/debugger/Breakpoints ` -file program.cs -line 25

There is also a condition parameter that causes a debug break for a specific condition, such as when the local variable “v” is less than 65:

new-item -path dte:/debugger/Breakpoints ` -file program.cs -line 25 ` -condition "v < 65"

That’s really the tip of the iceberg – breakpoints are very dynamic beasts.  In fact, I had no idea the extent to which you can customize breaking into the debugger until I started researching the Debugger node of the dte: drive.  To see all of your options, type:

get-help -path dte:/debugger/breakpoints new-item

You can remove breakpoints using the remove-item cmdlet; this example removes all breakpoints in the Program.cs file:

ls dte:/debugger/breakpoints ` | where{ $_.file -match 'program.cs' } ` | remove-item

You can also disable and enable breakpoints using the clear-item and invoke-item cmdlets, respectively:

$bkpts = ls dte:/debugger/breakpoints; # disable all breakpoints with a hitcount > 1 $bkpts | ?{ $_.currenthitcount -gt 1 } | clear-item; # enable all breakpoints in the Program.Main function $bkpts | ?{ $_.functionName -match 'Program.Main' } ` | invoke-item;

Debugged processes

When you start the debugger, the debuggedprocesses node of the dte: drive springs to life.  This node contains a list of all processes currently hooked by the debugger, so naturally its contents vary depending on what startup projects you have specified in your solution:

§ DTE:\debugger\DebuggedProcesses>ls Location: PSDTE::dte:\debugger\DebuggedProcesses Available Operations: d < ID Name ---------- -- ---- d < 4988 DemoSolution.vshost.exe

The DebuggedProcesses node doesn’t support many operations; it is simply a reflection of the current debugger state.  In fact, the only things you can do at this node is get-item (to fetch a lightweight process object) and set-location to enter a debugged process and start snooping around the threads:

§ DTE:\debugger\DebuggedProcesses>cd .\DemoSolution.vshost.exe § DTE:\debugger\DebuggedProcesses\DemoSolution.vshost.exe>ls Location: PSDTE::dte:\debugger\DebuggedProcesses\DemoSolution.vshost.exe Threads for Process: DemoSolution.vshost.exe Available Operations: d < Frozen Alive Thread ID Name ---------- ------ ----- --------- ---- d < * 3560 3560 d < * 1980 1980 d < * 464 vshost.RunParkingWindow d < * 5464 .NET SystemEvents d < * 5300 5300

Threads

Like processes, thread operations are limited to exploration: get-item will fetch a lightweight thread object for the specified thread, and set-location will drill into the stack for each thread:

§ ...\DemoSolution.vshost.exe>cd 5300 § ...\DemoSolution.vshost.exe\5300>ls Location: PSDTE::dte:\debugger\DebuggedProcesses\DemoSolution.vshost.exe\5300 Frames for Thread: Available Operations: d < Language Location Name ---------- -------- -------- ---- d < C# DemoSolution.Program.Main Current d < [Native to Managed Transition] Frame1 d < [Managed to Native Transition] Frame2 d < System.AppDomain.ExecuteAssembly Frame3 d < Microsoft.VisualStudio.HostingProcess... Frame4 d < System.Threading.ThreadHelper.ThreadS... Frame5 d < System.Threading.ExecutionContext.Run Frame6 d < System.Threading.ExecutionContext.Run Frame7 d < System.Threading.ThreadHelper.ThreadS... Frame8 d < [Native to Managed Transition] Frame9

Stack Frames

Stack frames are named by their current index, with the topmost frame always named “Current”, the next frame named “Frame1”, and so on.  Each frame is a container, so you can set-location again into a specific stack frame to probe further:

§ ...\DemoSolution.vshost.exe\5300>cd current § ...\DemoSolution.vshost.exe\5300\current>ls Container: PSDTE::dte:\debugger\DebuggedProcesses\DemoSolution.vshost.exe\5300\current Name ---------- ---- d < Arguments d < Locals

Which brings us to the meaty part…

Locals and Arguments

Once you navigate into a specific stack frames, you can access the local variables and arguments at that frame:

§ ...\5300\current>ls locals Location: PSDTE::dte:\debugger\DebuggedProcesses\DemoSolution.vshost.exe\5300\current\locals Available Operations: d < Value Type Name ---------- ----- ---- ---- < {string[0]} string[] args < 77 int v < "This is a v... string value

Once you’re this deep in the path hierarchy, you’re able to do some pretty neat things.  These locals values can be modified from the console, just like they can be modified from the locals or immediate window:

§ ...\5300\current>set-itemproperty -path v ` -name value -value 125 § ...\5300\current>ls Location: PSDTE::dte:\debugger\DebuggedProcesses\DemoSolution.vshost.exe\5300\current\locals Available Operations: d < Value Type Name ---------- ----- ---- ---- < {string[0]} string[] args < 125 int v < "This is a v... string value

Of course, being accessible in PowerShell means you gain so much more, such as the ability to search the stack for specific variables and values:

1 § ...\5300>$v = ls | ` #get all stack frames 2 join-path -child 'locals' | ` #create paths to local variables on each stack frame 3 ls | ` #list all locals... 4 where { $_.name -eq 'v' }#.. named "v" 5 § ...\5300>$v | select pspath,value 6 7 PSPath Value 8 ----- ----- 9 ...\Current\locals\v 123 10 ...\Frame1\locals\v 124 11 ...\Frame2\locals\v 125

This looks complex, but it simple when you break it down. 

  1. On line 1 we use get-childitems at the thread node to get a list of all stack frames. 
  2. Line 2 appends the node “locals” to each stack frame path, resulting in a list of StudioShell paths for the local variables at each stack frame. 
  3. Line 3 performs a get-childitems on each of these paths, resulting in a list of all locals across all stack frames. 
  4. Line 4 selects those local variables with a name of “v”. 
  5. Line 5 creates a simple report of the local variable path and its value. 

The result is the history of the values of variables named “v” at every stack location.  This isn’t something you can easily get from the IDE alone.

Looking Ahead

Hopefully this gives you a few ideas.  I’m still exploring this space in StudioShell, but I find myself giddy with ideas. 

One that’s got me drooling is the notion of replacing the breakpoint condition string with a scriptblock, enabling me to break at specific code locations in response to WMI events for instance.  Not sure how I will accomplish this yet – the condition doesn’t appear to be pluggable, but I’m sure there is something that can be done.