Back in my VBScript days, I had a script that would use Internet Explorer as a trace window. My script could run and messages would be written to an IE window. This was a handy way of separating debug or trace messages from the command output. When PowerShell came along I revised it. But IE is now a thing of the past. Although I still like the idea of a separate "trace" window. And now that PowerShell 7 supports Windows Presentation Foundation (WPF), on Windows platforms anyway, that felt like the right way to go.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Trace-Message
In the latest release of the PSScriptTools module, I've added a command called Trace-Message. The function is intended to be used within your code during the development process. The idea is that you can have Trace-Message commands in your code but they won't do anything unless a global variable called traceEnabled is set to True. If it is true, then you first call Trace-Message to spin up a new runspace and a WPF form. The command has parameters to allow some customization on the form's size and appearance. The form will automatically include some metadata about the user running the command. The function uses a synchronized hashtable to update the form so there is no blocking on your prompt.
The PSScriptTools module includes a few sample scripts that demonstrate how to use the commands. Here's one that uses Trace-Message.
#requires -version 5.1
#requires -module PSscripttools
$global:traceEnabled = $True
Trace-Message -title "Getting Counter Markdown" -Width 500 -BackgroundColor "#0fb93a"
Trace-Message "Starting Get-Mycounter"
$data = Get-MyCounter
if ($data) {
<#
Get the graphic from the images directory in the module.
Images in markdown work best when in the same folder as the markdown file
or use relative paths. The resulting markdown may not preview properly in
VS Code. You can test using Show-Markdown in PowerShell 7.x with the
-UseBrowser parameter
#>
$graphic = Get-Item "$PSScriptroot\..\images\db.png"
Trace-Message "Using graphic file from $($graphic.fullname)"
$graphicPath = $graphic.Fullname -replace "\\", "/"
Trace-Message "Formatted path to $GraphicPath"
$pre = @"
![graphic](file://$graphicPath)
## Computername: $($data[0].Computername)
"@
$post = "`nData collected _$($data[0].timestamp)_"
Trace-Message "Retrieved counter data from $($data[0].computername)"
Trace-Message "Generating markdown"
$file = Invoke-InputBox -Prompt "Where to do you want to save the file?" -Title "File Save"
if ($file) {
Trace-Message "Saving markdown to $file"
Try {
$data | Select-Object Category, Counter, Value |
ConvertTo-Markdown -Title "Performance Status" -PreContent $pre -AsTable -PostContent $post | Out-File -FilePath $file -Encoding utf8
Trace-Message "File saved"
Get-Item $file | Out-String | Trace-Message
}
Catch {
Trace-Message "Converting failed."
Trace-Message $_.Exception.message
}
} #if $file
else {
Trace-Message "No file specified"
}
} #if $data
Trace-Message "$($myinvocation.mycommand) completed"
Trace-Message "Disabling tracing"
$global:traceEnabled = $False
If $global:traceEnabled is false, the script runs and creates a markdown document with performance counter information using a few other commands from the module. But when I change it to true, here's what happens.
&$PSSamplePath\CounterMarkdown.ps1
The script will create a graphical input box that might be hidden behind other windows. But I will also get this trace window.
The timestamp is automatically added to each message.
I probably would want to adjust the form width in the script.
Trace-Message -title "Getting Counter Markdown" -Width 500 -BackgroundColor "#0fb93a"
Or stick to the default width of 800. If I click Save, I get the graphical SaveAs window where I can specify a file name. The contents of the trace window will be saved as a text file. If I was developing CounterMarkdown.ps1, I would set traceEnabled to False and be done. Until I had to troubleshoot or debug, in which case I could re-enable it.
A Verbose Alternative
However, there is another way you might use this function, assuming the PSScriptTools module is installed. I'm a big fan of using Verbose messages. But I could just as easily use Trace-Message. A few days ago I shared a function that demonstrated how you might find 0 length files using Get-CimInstance. Here's an updated version that lets me exclude a file extension and uses Trace-Message.
#requires -version 5.1
#requires -module PSScriptTools
Function Get-ZeroLengthFiles {
[CmdletBinding()]
[alias("gzf", "zombie")]
Param(
[Parameter(Position = 0)]
[ValidateScript({ Test-Path -Path $_ })]
[string]$Path = ".",
[Parameter(HelpMessage = "Exclude a file by extension like dat. Don't use a period.")]
[string]$ExcludeExtension,
[switch]$Recurse
)
Begin {
$cmdStart = Get-Date
If ($VerbosePreference -eq 'Continue') {
$global:traceEnabled = $True
Trace-Message -Title $($myinvocation.mycommand)
}
Trace-Message "[BEGIN ] Starting $($myinvocation.mycommand)"
#select a subset of properties which speeds things up
$get = "Name", "CreationDate", "LastModified", "FileSize"
$cimParams = @{
Classname = "CIM_DATAFILE"
Property = $get
ErrorAction = "Stop"
Filter = ""
}
Trace-Message "[BEGIN ] Using properties $($get -join ',')"
} #begin
Process {
Trace-Message "[PROCESS] Using specified path $Path"
if ($Recurse) {
Trace-Message "[PROCESS] Including subfolders in the search"
}
#test if folder is using a link or reparse point
if ( (Get-Item -Path $Path).Target) {
$target = (Get-Item -Path $Path).Target
Trace-Message "[PROCESS] A reparse point was detected pointing towards $target"
#re-define $path to use the target
$Path = $Target
}
#convert the path to a file system path
Trace-Message "[PROCESS] Converting $Path"
$cPath = Convert-Path $Path
Trace-Message "[PROCESS] Converted to $cPath"
#trim off any trailing \ if cPath is other than a drive root like C:\
if ($cpath.Length -gt 3 -AND $cpath -match "\\$") {
$cpath = $cpath -replace "\\$", ""
}
#parse out the drive
$drive = $cpath.Substring(0, 2)
Trace-Message "[PROCESS] Using Drive $drive"
#get the folder path from the first \
$folder = $cpath.Substring($cpath.IndexOf("\")).replace("\", "\\")
Trace-Message "[PROCESS] Using folder $folder (escaped)"
if ($folder -match "\w+" -AND $PSBoundParameters.ContainsKey("Recurse")) {
#create the filter to use the wildcard for recursing
$filter = "Drive='$drive' AND Path LIKE '$folder\\%' AND FileSize=0"
}
elseif ($folder -match "\w+") {
#create an exact path pattern
$filter = "Drive='$drive' AND Path='$folder\\' AND FileSize=0"
}
else {
#create a root drive filter for a path like C:\
$filter = "Drive='$drive' AND Path LIKE '\\%' AND FileSize=0"
}
if ($ExcludeExtension) {
Trace-Message "[PROCESS] Excluding file type: $excludeExtension"
$filter += " AND Extension <> '$excludeExtension'"
}
#add the filter to the parameter hashtable
$cimParams.filter = $filter
Trace-Message "[PROCESS] Looking for zero length files with filter: $filter"
#initialize a counter to keep track of the number of files found
$i = 0
$queryStart = Get-Date
Try {
Write-Host "Searching for zero length files in $cpath. This might take a few minutes..." -ForegroundColor magenta
#find files matching the query and create a custom object for each
Get-CimInstance @cimParams | ForEach-Object {
#increment the counter
$i++
#create a custom object
[PSCustomObject]@{
PSTypeName = "cimZeroLengthFile"
Path = $_.Name
Size = $_.FileSize
Created = $_.CreationDate
LastModified = $_.LastModified
}
}
}
Catch {
Write-Warning "Failed to run query. $($_.exception.message)"
}
Trace-Message "[PROCESS] Query completed in $((Get-Date) - $queryStart)"
if ($i -eq 0) {
#display a message if no files were found
$msg = "No zero length files were found in $cpath."
Trace-Message "[PROCESS] $msg"
Write-Host -ForegroundColor yellow
}
else {
Trace-Message "[PROCESS] Found $i matching files"
}
} #process
End {
Trace-Message "[END ] Command completed in $((Get-Date) - $cmdStart)"
Trace-Message "[END ] Ending $($myinvocation.mycommand)"
if ( $global:traceEnabled) {
$global:traceEnabled = $False
}
} #end
}
I changed all of my original Write-Verbose lines to Trace-Message and removed the timestamp from the original message since Trace-Message inserts that for me automatically. If the user runs this function with -Verbose, the trace code will fire up and I'll get the WPF form. This time, I'm using the default values for the form.
Get-ZeroLengthFiles -Path c:\scripts -Recurse -exclude psrc -Verbose
The only verbose output at the prompt is that from Get-CimInstance. All my other trace/ verbose messages are in the trace form.
If someone was having problems running this function, I could have them run the command with -Verbose, save the output and send me the file.That's where the metadata at the beginning can be useful. And yes, they would need the PSScriptTools module installed locally. Or at the very least, you could grab the Trace-Window code and include it in our own module.
I should point out that there is a slight performance penalty when using Trace-Message. But, I'm assuming you are only using it to troubleshoot or debug.
You can install PSSscriptTools from the PowerShell Gallery. Most commands should work in Windows PowerShell and PowerShell 7. I hope you'll give this a spin and let me know what you think.