Tuesday, January 22, 2008 8:21 PM
jrobbins
PowerShell Script for Setting Symbol Paths
In my continued resolution to make PowerShell my full time command line interface, I've been porting over some of my batch and JavaScript files as a learning exercise. Since the only way to really learn a technology is to do something real with it, this has been some helpful practice to "become one with the PowerShell." I'll blog later about some of the tips and tricks I've learned writing PowerShell scripts and using it day to day. The good news for the PowerShell team is that I'm still sticking with my New Year's resolution long past the point where most people quit.
Speaking of New Year's resolutions, nearly everyone makes the resolution to get into shape or lose weight. I was talking with an owner of several gyms about how many people stick with their fitness resolutions and the statistics are that somewhere like 90% of the people that join a gym in January stop going within three weeks. It turns out that fitness industry has a name for these people: pure profit.
The first thing I do on any computer I touch is set the _NT_SYMBOL_PATH environment variable. No matter if you're doing .NET or the most hard core native C++, you need to have the environment variable set to have the debuggers and tools like Process Explorer getting the symbols for your builds and the Microsoft public symbols. It's all about getting good call stacks when debugging. If you don't have good call stacks, you're debugging challenges grow exponentially. While I personally think that _NT_SYMBOL_PATH should be automatically set by the operating system, I won't hold my breath waiting for that to happen.
While the Visual Studio debuggers can use _NT_SYMBOL_PATH, there are advantages to specifically tweaking the Visual Studio registry keys. The main one is that by setting the Visual Studio Options dialog, Debugging, Symbols property page to point to your symbol servers the settings apply for both symbols and binaries from your minidumps. Secondly, with the new and completely outstanding .NET Reference Source project, having Visual Studio set to step all through Console.WriteLine is beyond cool. (What!? Microsoft also released the source code to things like System.Web.DLL and Windows.Forms.DLL? Why would anyone chose a complicated interface like those versus complete text nirvana?)
For many years, I've been copying a JavaScript file off my USB key that would set _NT_SYMBOL_PATH and Visual Studio symbol paths on any machine I touched. Now that I'm installing PowerShell on everything before I do anything else, I rewrote the script in PowerShell and called it Set-SymbolServer. You can see the script at the end of this blog entry. I wrote Set-SymbolServer as a script file because I wasn't doing anything with the pipeline. In my quest for PowerShell black belt ninja status, I guess this script means I'm possibly at a pink belt level. Also, as Set-SymbolServer is one of my first scripts, it's probably not that smart in the way of PowerShell so please slam away in the comments for ways I can make it better.
When running Set-SymbolServer it assumes you are setting up Visual Studio 2008. If you are still using Visual Studio 2005, use the –Vs2005 switch. The only require parameters are either –public or –internal. Those dictate if you're setting up the machine for using the Microsoft public symbol servers or an internal company symbol server respectively. With –public, the download cache will be set to C:\SYMBOLS\OSYMBOLS and the _NT_SYMBOL_PATH environment variable will be set to use both http://referencesource.microsoft.com/symbols and http://msdl.microsoft.com/download/symbols. If Visual Studio 2008 is installed, its registry keys will be set to use the two Microsoft symbol servers. Additionally, the appropriate Visual Studio 2008 registry keys will be set to use the Source Server (needed for the Reference .NET Source), and Just My Code is turned off. If you specified –Vs2005, the symbol server is set to http://msdl.microsoft.com/download/symbols and no other registry settings are set.
Specifying –internal will set the _NT_SYMBOL_PATH and Visual Studio to use a cache of c:\Symbols\InternalSymbols and a symbol server of \\Symbols\Symbols. That flag will not touch any other Visual Studio settings. These same settings apply if you specify –Vs2005.
If you want a different cache directory, specify it with the –CacheDirectory parameter. If you have additional symbol servers you want add, specify them as an array to the –SymbolServers parameter. If you get confused as to the options, pass -? and you'll get the usage test to remind you.
There are some additional command line options to Set-SymbolServer; –WhatIf, –Confirm, and –Verbose. A totally phenomenal feature of PowerShell is seeing what a particular command or cmdlet will affect. It's been hugely instructive as a learning aid so I wanted all my scripts to support the default convention. Fortunately, Jeffrey Snover, the one man blog comment factory, posted a great example of how to implement those key parameters in a script. I slightly modified Jeffrey's function in my script to access the script parameters directly instead of requiring them to be passed in to each call.
I hope you find the script useful and please do let me know where parts could be implemented better.
###############################################################################
# Set-SymbolServer - Sets up the current user account to use a symbol server.
#
# Copyright (c) 2008 - John Robbins (john@wintellect.com)
#
# Version 1.0 - Jan 22, 2008
###############################################################################
param ( [switch] $Internal ,
[switch] $Public ,
[switch] $Vs2005 ,
[string] $CacheDirectory ,
[string[]] $SymbolServers ,
[switch] $Verbose ,
[switch] $Confirm ,
[switch] $Whatif )
# Always make sure all variables are defined.
Set-PSDebug -Strict
# The reference variable used to determine if the user pressed Y/A.
$script:AllAnswer = $null
function Usage
{
""
"Usage: Set-SymbolServer [-Internal] [-Public] [-Vs2005]"
" [[-CacheDirectory] <string>]"
" [[-SymbolServers] <string array>]"
" [-Verbose] [-Confirm] [-WhatIf]"
""
"Parameters:"
" -Internal : Set up for using an internal symbol server"
" (file://symbols/symbols)"
" -Public : Use the public Microsoft symbol servers."
" -Vs2005 : Set the symbol server settings for Visual Studio 2005"
" instead of the default Visual Studio 2008."
" -CacheDirectory : Use the specified download cache directory instead of the"
" default (internal: c:\symbols\internalsymbols,"
" public: c:\symbols\ossymbols)."
" -SymbolServers : Additional symbol servers to add to the defaults."
" -? : Display this usage information"
""
" Note that either -Public or -Internal must be specified"
""
exit
}
# A modified version of Jeffrey Snover's Should-Process script.
# http://blogs.msdn.com/powershell/archive/2007/02/25/supporting-whatif-confirm-verbose-in-scripts.aspx
function Should-Process ( $Operation ,
$Target ,
[REF]$AllAnswer ,
$Warning = "" )
{
if ($AllAnswer.Value -eq $FALSE)
{
return $FALSE
}
elseif ($AllAnswer.Value -eq $TRUE)
{
return $TRUE
}
if ($Whatif)
{
Write-Host "What if: Performing operation `"$Operation`" on Target `"$Target`""
return $FALSE
}
if ($Confirm)
{
$ConfirmText = @"
Confirm Are you sure you want to perform this action?
Performing operation "$Operation" on Target "$Target". $Warning
"@
Write-Host $ConfirmText
while ($TRUE)
{
$answer = Read-Host @"
[Y] Yes [A] Yes to All [N] No [L] No to all [S] Suspend [?] Help (default is "Y")
"@
switch ($Answer)
{
"Y" { return $TRUE}
"" { return $TRUE}
"A" { $AllAnswer.Value = $TRUE; return $TRUE }
"N" { return $FALSE }
"L" { $AllAnswer.Value = $FALSE; return $FALSE }
"S" { $Host.EnterNestedPrompt();
Write-Host $ConfirmText }
"?" { Write-Host @"
Y - Continue with only the next step of the operation.
A - Continue with all the steps of the operation.
N - Skip this operation and proceed with the next operation.
L - Skip this operation and all subsequent operations.
S - Pause the current pipeline and return to the command prompt. Type "exit" to resume the pipeline.
"@
}
}
}
}
if ($verbose)
{
Write-Verbose "Performing `"$Operation`" on Target `"$Target`"."
}
return $TRUE
}
function Set-ItemPropertyScript ( $path , $name , $value )
{
$propString = "Item: " + $path.ToString() + "Property: " + $name
if ( Should-Process "Set Property" $propString ([REF]$AllAnswer) )
{
Set-ItemProperty -Path $path -Name $name -Value $value
}
}
function Set-ItemPropertyTypeScript ( $path , $name , $value , $type )
{
$propString = "Item: " + $path.ToString() + "Property: " + $name
if ( Should-Process "Set Property" $propString ([REF]$AllAnswer) )
{
Set-ItemProperty -Path $path -Name $name -Value $value -Type $type
}
}
# Creates the cache directory if it does not exist.
function CreateCacheDirectory ( [string] $cacheDirectory )
{
if ( ! $(Test-path $cacheDirectory -type "Container" ))
{
if ( Should-Process "New-Item" $cacheDirectory ([REF]$AllAnswer) )
{
New-Item -type directory -Path $cacheDirectory > $null
}
}
}
function WriteSymbolEnvironment ( [string] $envValue )
{
Set-ItemPropertyScript hkcu:\Environment _NT_SYMBOL_PATH $envValue
}
function SetSymbolServer ( )
{
# Set the defaults. Assuming VS2008 and public symbol servers.
$realCacheDir = "c:\Symbols\OSSymbols"
# This function assumes $symServers is an array.
$symServers = "http://referencesource.microsoft.com/symbols","http://msdl.microsoft.com/download/symbols"
# Am I doing this for VS 2005?
if ( $Vs2005 )
{
$symServers = , "http://msdl.microsoft.com/download/symbols"
}
# Am I supposed to do an internal company symbol server?
if ( $Internal )
{
$realCacheDir = "c:\Symbols\InternalSymbols"
$symServers = , "\\symbols\symbols"
}
# If the user specified a cache directory, that takes precedence over all.
if ( $CacheDirectory.Length -gt 0 )
{
$realCacheDir = $CacheDirectory
}
# Add on any additional symbol servers the user set.
if ( $SymbolServers.Length -gt 0 )
{
$symServers += $SymbolServers
}
Write-Debug "SSSfn: realCacheDir = $realCacheDir"
Write-Debug "SSSfn: symServers = $symServers"
CreateCacheDirectory ( $realCacheDir )
# Prepare and set the user's _NT_SYMBOL_PATH environment variable.
$envValue = "SRV*" + $realCacheDir + "*" + [string]::Join('*' , $symServers)
Write-Debug "SSSfn: envValue = $envValue"
WriteSymbolEnvironment ( $envValue )
# The debugger's registry key
$verNumber = $( if ( $Vs2005 ) { "8" } else { "9" } )
$dbgRegKey = "HKCU:\Software\Microsoft\VisualStudio\{0}.0\Debugger" -f $verNumber
Write-Debug "SSSfn: RegKey = $dbgRegKey"
$symsrvString = $( if ( $Public ) { "the public Microsoft" } else { "an internal" } )
# Look to see if the registry key exists. If it does not, VS is not installed.
if ( $( Test-Path $dbgRegKey ) )
{
# Write the registry keys common between VS 2005 and VS 2008.
# First, do the SymbolPath values.
$symPathValue = [string]::Join(";" , $symServers)
Write-Debug "SSSfn: SymbolPath = $symPathValue"
Set-ItemPropertyScript $dbgRegKey SymbolPath $symPathValue
# Second, enable those symbol paths. This will show them as checked
# in the Options dialog, Debugger, Symbols page.
# Gotta deal with the typelessness of PowerShell.
$pathStateValue = ""
for ( $i = 0 ; $i -lt $symServers.Length ; $i++ )
{
$pathStateValue += "1"
}
Write-Debug "SSSfn: SymbolPathState = $pathStateValue"
Set-ItemPropertyScript $dbgRegKey SymbolPathState $pathStateValue
# Finally, set the cache directory.
Write-Debug "SSSfn: SymbolCacheDir = $realCacheDir"
Set-ItemPropertyScript $dbgRegKey SymbolCacheDir $realCacheDir
# If we're doing the public symbol server and VS 2008, enable
# source server support and turn off Just My Code.
if ( ( ! $Vs2005 ) -and ( ! $Internal ) )
{
Write-Debug "SSSfn: VS08 JustMyCode = 0"
Set-ItemPropertyTypeScript $dbgRegKey JustMyCode 0 DWORD
Write-Debug "SSSfn: VS08 UseSourceServer = 1"
Set-ItemPropertyTypeScript $dbgRegKey UseSourceServer 1 DWORD
}
""
"Updated Visual Studio 200{0} to use {1} symbol server(s)." -f
$( if ( $Vs2005 ) { "5" } else { "8" } ) ,
$symsrvString
}
"Added the current user _NT_SYMBOL_PATH environment variable to use"
"{0} symbol server(s)." -f $symsrvString
""
"Please log out to activate the new symbol server settings."
""
}
# Check for the help request.
if ( ( $Args -eq '-?') -or ( ( ! $Internal ) -and ( ! $Public ) ) )
{
Usage
}
if ( $Internal -and $Public )
{
Throw "Only one of -Public or -Internal can be specified"
}
Write-Debug "Param Internal = $Internal"
Write-Debug "Param Public = $Public"
Write-Debug "Param Vs2005 = $Vs2005"
Write-Debug "Param CacheDirectory = $CacheDirectory"
Write-Debug "Param SymbolServers = $SymbolServers"
Write-Debug "Param Verbose = $Verbose"
Write-Debug "Param Confirm = $Confirm"
Write-Debug "Param Whatif = $Whatif"
SetSymbolServer