I just love PowerShell! One of the things I love the most about it is that you can pick the level of programming you want, write a script and then evolve it to meet whatever need you have. Sometimes you want something quick and dirty and other times you want something you’ll share with others and then other times you want something that is going to be used in production.
When I was earning my living on Unix systems, I used to write quick and dirty CSH scripts (yeah – I was one of those guys) but then when I wanted something more formal or to share with others, often I would throw away the script and restart from scratch. I don’t do that with PowerShell.
This morning I was reviewing someone’s scripts that they were going to use to perform a large scale upgrade to a datacenter. The code had lots of validation and verbose output and then they had lots of code that looked like this:
- #Start Stopwatch
- $sw = [Diagnostics.Stopwatch]::StartNew()
- #…..
- $sw.Stop()
- Write-Verbose “Total ElapsedTime: $($sw.Elapsed.TotalSeconds) Seconds”
I thought that I would help out by providing a helper function so I wrote my first version of the script like this:
- Function Measure-Time
- {
- param(
- #ScriptBlock that you want to measure the timing of
- [Parameter(Position=0, Mandatory=1)]
- [ScriptBlock]$ScriptBlock
- )
- $StopWatch = [Diagnostics.Stopwatch]::StartNew()
- &$ScriptBlock
- $StopWatch.Stop()
- Write-Verbose “Total ElapsedTime: $($StopWatch.Elapsed.TotalSeconds) Seconds”
- }
- Set-Alias Time Measure-Time
- #Example usage:
- Time {gps *ss} -Verbose
So that worked great but then I thought about some problems:
- Using the variables $Stopwatch and $ScriptBlock, it meant that those variables couldn’t be used in the scriptblock that was passed in. Whenever you have a situation like this, the convention is to put 2 “_” in front of a name so I renamed them $__ScriptBlock and $__StopWatch.
- I didn’t have any help. I can never remember the keywords to add comment-based help so what I do is to start a new script, type CTRL-J to bring up snippets, select the CMDLET snippet and then cut/paste the help from it and use it to fill it in.
- I wanted the user to be able to specify their own message so I provided a -__FormatString variable.
- If you are upgrading a datacenter, you want to use the belt and suspenders approach so I thought this information should be logged to the eventlog (this started the next leg of the journey but more about that later).
The resultant script was starting to look like something worth sharing:
- <#
- .Synopsis
- Measure how long something takes
- .DESCRIPTION
- This script measures how long it takes to run a sequence of code (a ScriptBlock).
- The message is sent to the Verbose channel so to see it, you either need to have
- $VerbosePreference set to “Continue” or specify -Verbose.
- .EXAMPLE
- Measure-Time { get-process } -Verbose
- .EXAMPLE
- time {get-process} DataCenterUpgrade “It took {0:N3} seconds to run this” -verbose
- #>
- Function Measure-Time
- {
- param(
- #ScriptBlock that you want to measure the timing of
- [Parameter(Position=0, Mandatory=1)]
- [ScriptBlock]$__ScriptBlock,
- # Name of the LogSource in the APPLICATION EventLog
- # that these events will show up in.
- #
- # If you don’t provide a value – no log entry will be made.
- [Parameter(Position=1)]
- [String]$__LogSource = $null,
- # An alternate format string for the Message
- [Parameter(Position=2)]
- [String]$__FormatString=“Total ElapsedTime: {0:N1} Seconds”
- )
- $_StopWatch = [Diagnostics.Stopwatch]::StartNew()
- &$__ScriptBlock
- $_StopWatch.Stop()
- $Message = $__FormatString -f $_StopWatch.Elapsed.TotalSeconds
- Write-Verbose $Message
- if ($__LogSource)
- {
- Write-EventLog -LogName Application `
- -Source $__LogSource `
- -EntryType Information `
- -EventId 1 -Message $Message
- }
- }
- Set-Alias -Name Time -Value Measure-Time
- #Example usage:
- Time {gps *ss} DataCenterUpgrade -Verbose
So everything looked great until I went to run it and then it barfed complaining that there was no DataSource called “DataCenterUpgrade”. I’ve always believed that the different between a quick and dirty script and a production script was the error handling. When you are in a datacenter at 3am when you expected to be home at 7pm the previous evening and everything it turning to crap – you want to have GREAT error messages.
I started off by adding some code to check for the DataSource and create it if it didn’t exist:
- if ($__LogSource)
- {
- if (![System.Diagnostics.EventLog]::SourceExists($__LogSource))
- {
- [System.Diagnostics.EventLog]::CreateEventSource($__LogSource, “Application”)
- }
- }
The problem with that is that it only works if you are running with Admin privs (which I wasn’t of course). So I put in a TRY/Catch and spent some time crafting an excellent error message:
- if ($__LogSource)
- {
- try {
- if (![System.Diagnostics.EventLog]::SourceExists($__LogSource))
- {
- [System.Diagnostics.EventLog]::CreateEventSource($__LogSource, “Application”)
- }
- Write-EventLog -LogName Application -Source $__LogSource `
- -EntryType Information -EventId 1 -Message $Message
- }
- catch [Exception] # An exception means you don’t have the privs to create the source
- {
- Write-Error -Message “Cannot Write to or Create EventLogSource ($__LogSource)” `
- -Exception $_.Exception `
- -Category ResourceUnavailable `
- -TargetObject $__LogSource `
- -RecommendedAction “Create LogSource ($__LogSource) using an account with Admin privs” `
- -ErrorId “CantWriteLog,Measure-Time” `
- -CategoryReason CantFindorCreateLogSource
- }
- }
This produced a great error message so I was feeling pretty good about my script. Next I decided to look at the EventLog and realized that it was now logging perfectly accurate and perfectly useless information. It was saying how long it took to do something but not how long it took to do WHAT? Now I could have handled this using the FormatString:
- Time {gps *ss} DataCenterUpgrade “It took {0:N} to Gps”
- Time {Step2 } DataCenterUpgrade “It took {0:N} to do STEP2”
But that looks like a horrible user experience so I decided to include the scriptblock in the eventlog. It then hit me that I had no idea how long the scriptblock would be but that it I was sure that the eventlog had a limit on the maximum size of the entry. I could have researched this but I decided to experiment instead. I created a DataSource and then did a number of experiments like this:
- $x = “T” * 1kb
- Write-EventLog -LogName Application -Source MySource $x
- $x = “T” * 10kb
- Write-EventLog -LogName Application -Source MySource $x
- $x = “T” * 100kb
- Write-EventLog -LogName Application -Source MySource $x
From this I determined that the max size was 32KB. I added code to only log the first 31KB of a scripblock. Now any reasonable person would point out that a 32KB scriptblock is enormous and we’ll never see this condition.
Here is the point – that MAY be true. But if it isn’t true and it breaks at 3am while some poor soul is trying to stich his datacenter together, then I’ve just made his life even worse. And so we always code for that poor soul because one day, we will be that guy. (Remind me to tell you the story about having to sleep on the datacenter floor in a hospital in Memphis between bouts of hand stitching a database together while patients were delayed getting admitted for care.) So here is how that segment came out:
- if ($__LogSource)
- {
- try {
- if (![System.Diagnostics.EventLog]::SourceExists($__LogSource))
- {
- [System.Diagnostics.EventLog]::CreateEventSource($__LogSource, “Application”)
- }
- # Maximum size of a Eventlog Message is 32KB. We limit to 31KB.
- $Script = $__ScriptBlock.ToString()
- if ($Script.Length -ge 31kb)
- {
- $Script = $Script.Substring(0, 31kb)
- }
- Write-EventLog -LogName Application -Source $__LogSource `
- -EntryType Information -EventId 1 -Message $(
- $Message + ” to run `n” + $Script
- )
- }
- catch [Exception] # An exception means you don’t have the privs to create the source
- {
- Write-Error -Message “Cannot Write to or Create EventLogSource ($__LogSource)” `
- -Exception $_.Exception `
- -Category ResourceUnavailable `
- -TargetObject $__LogSource `
- -RecommendedAction “Create LogSource ($__LogSource) using an account with Admin privs” `
- -ErrorId “CantWriteLog,Measure-Time” `
- -CategoryReason CantFindorCreateLogSource
- }
- }
So the point of this blog is that with PowerShell, you can start off with a quick and dirty and evolve it to meet whatever the requirements you have without having to start over from scratch. It is one of my favorite attributes of PowerShell. It should be – we designed it to do this. 🙂 So below is the script in the state I left it today. There is more that can be done but it is fit for the purpose it is going to be used for. We don’t have to make every script be model of perfection.
- <#
- .Synopsis
- Measure how long something takes
- .DESCRIPTION
- This script measures how long it takes to run a sequence of code (a ScriptBlock).
- The message is sent to the Verbose channel so to see it, you either need to have
- $VerbosePreference set to “Continue” or specify -Verbose.
- .EXAMPLE
- Measure-Time { get-process } -Verbose
- .EXAMPLE
- time {get-process} DataCenterUpgrade “It took {0:N3} seconds to run this” -verbose
- #>
- Function Measure-Time
- {
- param(
- #ScriptBlock that you want to measure the timing of
- [Parameter(Position=0, Mandatory=1)]
- [ScriptBlock]$__ScriptBlock,
- # Name of the LogSource in the APPLICATION EventLog
- # that these events will show up in.
- #
- # If you don’t provide a value – no log entry will be made.
- [Parameter(Position=1)]
- [String]$__LogSource = $null,
- # An alternate format string for the Message
- [Parameter(Position=2)]
- [String]$__FormatString=“Total ElapsedTime: {0:N1} Seconds”
- )
- $_StopWatch = [Diagnostics.Stopwatch]::StartNew()
- &$__ScriptBlock
- $_StopWatch.Stop()
- $Message = $__FormatString -f $_StopWatch.Elapsed.TotalSeconds
- Write-Verbose $Message
- if ($__LogSource)
- {
- try {
- if (![System.Diagnostics.EventLog]::SourceExists($__LogSource))
- {
- [System.Diagnostics.EventLog]::CreateEventSource($__LogSource, “Application”)
- }
- # Maximum size of a Eventlog Message is 32KB. We limit to 31KB.
- $Script = $__ScriptBlock.ToString()
- if ($Script.Length -ge 31kb)
- {
- $Script = $Script.Substring(0, 31kb)
- }
- Write-EventLog -LogName Application -Source $__LogSource -EntryType Information -EventId 1 -Message $(
- $Message + ” to run `n” + $Script
- )
- }
- catch [Exception] # An exception means you don’t have the privs to create the source
- {
- Write-Error -Message “Cannot Write to or Create EventLogSource ($__LogSource)” `
- -Exception $_.Exception `
- -Category ResourceUnavailable `
- -TargetObject $__LogSource `
- -RecommendedAction “Create LogSource ($__LogSource) using an account with Admin privs” `
- -ErrorId “CantWriteLog,Measure-Time” `
- -CategoryReason CantFindorCreateLogSource
- }
- }
- }
- Set-Alias -Name Time -Value Measure-Time
I hope sharing my thought process was useful for your scripting.
Cheers!
Jeffrey
Instead of using __ prefixes, wouldn’t $private: work?
Great post Jeffrey. Starting to follow this blog by the recently killed Google reader 🙂
“From this I determined that the max size was 32KB. I added code to only log the first 31KB of a scripblock. Now any reasonable person would point out that a 32KB scriptblock is enormous and we’ll never see this condition.
Here is the point – that MAY be true. But if it isn’t true and it breaks at 3am while some poor soul is trying to stich his datacenter together, then I’ve just made his life even worse. And so we always code for that poor soul because one day, we will be that guy.”
This is so true. I’d take your earlier statement a step farther: the difference between a quick and dirty script and a production script is handling errors AND boundary cases properly. It’s easy to write code that works most of the time; writing code that works all of the time takes a bit more attention to detail.
What if I learned PowerShell so well so that when I went to a new job I didn’t scare colleagues when I talked about how good and powerful PowerShell is. sounds funny! No, it is what’s happening and to be honest I am afraid loosing my job over it. Still in Love…With my PowerShell – I picked up what PowerShell is designed to do to get people like me to make tools for the job but if that created aggressive competition not sure what to do…Sounds like it is time to come up with some Microsoft official PowerShell exams