Revisiting the PowerShell Uptime Clock

We have been on a journey of PowerShell exploration. Over the last few articles, we’ve gone from a few lines of PowerShell code that you could type at the prompt to a reusable PowerShell function. Be sure to get caught up if you are joining us partway through the journey.

Remember the end result isn’t as important as what you learn along the way. The last version I showed of the Get-MyUptime command is pretty complete, but there are a few more final touches that we can apply. Let me show you the next version and then I’ll explain.

​
<#
.Synopsis
Get computer uptime.
.Description
This command will query the Win32_OperatingSystem class using Get-CimInstance and write an uptime object to the pipeline.
.Parameter Computername
The name of the computer to query. This parameter has an alias of CN. The computer must be running PowerShell 3.0 or later.
.Parameter Test
use Test-WSMan to verify computer can be reached and is running v3 or later of the Windows Management Framework stack.
.Example
PS C:\> get-myuptime chi-dc01
Computername   : CHI-DC01
LastRebootTime : 11/15/2014 12:02:22 AM
Days           : 23
Hours          : 17
Minutes        : 23
Seconds        : 14
Default output for a single computer.
.Example
PS C:\> get-myuptime chi-dc01,chi-fp02 | format-table
Computername LastRebootTime         Days Hours Minutes Seconds
------------ --------------         ---- ----- ------- -------
CHI-DC01     11/15/2014 12:02:22 AM   23    17      23      44
CHI-FP02     12/1/2014 8:40:08 AM      7     8      45      58
Formatted results for multiple computers. You can also pipe computer names into this command.
.Notes
Last Updated: December 8, 2014
Version     : 1.0
Learn more about PowerShell:
Essential PowerShell Learning Resources
.Link Get-Ciminstance Get-WMIObject .Link https://petri.com/powershell #> [cmdletbinding()] Param( [Parameter(Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateNotNullorEmpty()] [Alias("cn","name")] [String[]]$Computername = $env:Computername, [Switch]$Test ) Begin { Write-Verbose "Starting $($MyInvocation.Mycommand)" #define a private function to be used in this command Function IsWsManAvailable { [cmdletbinding()] Param([string]$Computername) Write-Verbose "Testing WSMan for $computername" Try { $text = (Test-WSMan $computername -ErrorAction Stop).lastchild.innertext Write-Verbose $Text if ($text.Split(":")[-1] -as [double] -ge 3) { $True } else { $False } } Catch { #failed to run Test-WSMan most likely due to name resolution or offline $False } } } #begin Process { Foreach ($computer in $computername) { Write-Verbose "Processing $($computer.toUpper())" if ($Test -AND (IsWsManAvailable -Computername $computer)) { $OK = $True } elseif ($Test -AND -Not (IsWsManAvailable -Computername $computer)){ $OK = $False Write-Warning "$($Computer.toUpper()) is not accessible" } else { #no testing so assume OK to proceed $OK = $True } #get uptime of OK if ($OK) { Write-Verbose "Getting uptime from $($computer.toupper())" Try { $Reboot = Get-CimInstance -classname Win32_OperatingSystem -ComputerName $computer -ErrorAction Stop | Select-Object -property CSName,LastBootUpTime } Catch { Write-Warning "Failed to get CIM instance from $($computer.toupper())" Write-Warning $_.exception.message } if ($Reboot) { Write-Verbose "Calculating timespan from $($reboot.LastBootUpTime)" #create a timespan object and pipe to Select-Object New-TimeSpan -Start $reboot.LastBootUpTime -End (Get-Date) | Select-Object @{Name = "Computername"; Expression = {$Reboot.CSName}}, @{Name = "LastRebootTime"; Expression = {$Reboot.LastBootUpTime}},Days,Hours,Minutes,Seconds } #if $reboot } #if OK #reset variable so it doesn't accidentally get re-used, especially when using the ISE #ignore any errors if the variable doesn't exit Remove-Variable -Name Reboot -ErrorAction SilentlyContinue } #foreach } #process End { Write-Verbose "Ending $($MyInvocation.Mycommand)" } #end } #end function

The first major change is that I inserted comment-based help. Even though it is a comment, PowerShell will process it. Here’s an outline of the commonly used sections.

​
.Synopsis
.Description
.Parameter XXX
.Example
.Notes
.Link
#>

At minimum I would suggest using Synopsis, Description, and one example. Be careful with your formatting and spelling, as there is a period before each entry. Some people put the heading in upper case to make it easier to read. As long as it is spelled properly, PowerShell doesn’t care. Unfortunately, if there is a typo in one of the headings or syntax error, then PowerShell won’t display an error, nor will it show your help content. The headings are the same that you see in cmdlet help. When you write the description, you don’t have to worry about line breaks. I tend to write the description in the ISE until I reach the logical end of a paragraph. It will be a long series of lines in the ISE, but when PowerShell displays the help, then it will automatically format line length for me.

PowerShell will detect and automatically display your parameters and syntax. Additionally, you can include a parameter part to your help. In my function, I have a parameter help definition for Computername.

​
You can also have as many examples as you'd like. You don't need to number them, just add example sections. For example, I include the PS prompt in my example. It isn't required, but if you only insert your command, the help system will generate a prompt of C:\PS>, which looks like the PS directory to me. Personally, I prefer my example prompt to look like my console: PS C:\>.
The link section is for related cmdlets. You can have as many cmdlet names as you want under a single Link heading. If you want to provide a link to online help for something, put that in a separate link. When you are finished, the person running your command can ask for help like any other command.
Related links for Windows PowerShell cmdlets. (Image Credit: Jeff Hicks)
Related links for Windows PowerShell cmdlets. (Image Credit: Jeff Hicks)
  They can even use help's Show-Window parameter.
Help for Get-MyUptime cmdlet. (Image Credit: Jeff Hicks)
Help for Get-MyUptime cmdlet. (Image Credit: Jeff Hicks)
  Next, let's look at a few other changes I made to make this even more resilient and easy to use. The heart of the function is the Get-CimInstance command. I am using Try/Catch statement to properly handle errors. One of the drawbacks with my previous version that it wasn't always easy to tell which error message went with which computer name. So instead of simply writing the error object, I like to use Write-Warning.
​ Catch {
    Write-Warning "Failed to get CIM instance from $($computer.toupper())"
    Write-Warning $_.exception.message
}

The first line will let me know what computer failed, and the second will display the relevant error message. I rarely need the complete exception object. But if I did, I can still find it in the global $error variable.
My other major change is to add a parameter and some logic to test the computer before I attempt to use Get-CimInstance. You can think of this process as pre-error handling. Again, the method in which I’m using it is more important than the end result. I could have used Test-Connection to ping the computer, but Get-CimInstance really needs the remote computer to be running PowerShell 3.0 or later. I can get that information using Test-WSMan.

Using Test-WSMan in Windows PowerShell. (Image Credit: Jeff Hicks)
Using Test-WSMan in Windows PowerShell. (Image Credit: Jeff Hicks)

 
I can figure out the version from the highlighted values. Because obtaining that value is a little tricky, I wrote a function to check the version. If my function determines if the version is 3.0 or later, then it writes $True to the pipeline.

Function IsWsManAvailable {
    [cmdletbinding()]
    Param([string]$Computername)
    Write-Verbose "Testing WSMan for $computername"
    Try {
        $text = (Test-WSMan $computername -ErrorAction Stop).lastchild.innertext
        Write-Verbose $Text
        if ($text.Split(":")[-1] -as [double] -ge 3) {
            $True
        }
        else {
            $False
        }
    }
    Catch {
        #failed to run Test-WSMan most likely due to name resolution or offline
        $False
    }
    }

I’m not using a standard naming convention because this is a function that I only intend to use within Get-MyUptime. It has to be defined before I can call it so I insert it into the begin block. This defines the function before any pipelined names are processed. Within the process block, I’ve written logic to determine if the computer has the proper version, assuming the person running the script used the –Test parameter.

​ if ($Test -AND (IsWsManAvailable -Computername $computer)) {
    $OK = $True
    }
elseif ($Test -AND -Not (IsWsManAvailable -Computername $computer)){
    $OK = $False
    Write-Warning "$($Computer.toUpper()) is not accessible"
    }
else {
    #no testing so assume OK to proceed
    $OK = $True
}

It is possible that a computer could test OK for WSMan, but still fail with Get-CimInstance, which is why I still have error handling with Try/Catch. Let’s put it all to the test.

​$c = "chi-dc01","chi-dc02","chi-dc04","foo","chi-fp02","chi-core01"
get-myuptime $c | sort Computername | format-table

Warning in Windows PowerShell. (Image Credit: Jeff Hicks)
Warning in Windows PowerShell. (Image Credit: Jeff Hicks)

 
You can see the errors now as warnings. Or I can test.

​get-myuptime $c -test | sort days,computername |
format-table -group Days -property Computername,LastRebootTime,
@{Name="Uptime";Expression={new-timespan -Days $_.Days -hours $_.Hours -Minutes $_.Minutes}}

In this example I also decided to group the results by the number of days, and I turned the uptime back into a string.

Grouping results by number of days in Windows PowerShell. (Image Credit: Jeff Hicks)
Grouping results by number of days in Windows PowerShell. (Image Credit: Jeff Hicks)

 
And remember those aliases for the Computername parameter? You may have noticed a few changes in this version. Because I decided that the person running this tool will most likely be querying Active Directory for computer names, I wanted to make it as easy as possible to connect the different cmdlets together in a single command.

The object from Get-ADComputer doesn’t have a Computername parameter, but it does have one called Name. All of this allows me to do this:

​start-job { get-adcomputer -filter * | select name | Get-MyUptime -test} -InitializationScript {. c:\scripts\get-myuptime.ps1} -Name Uptime
receive-job uptime –keep | Select * -exclude RunspaceID | Out-GridView -title "Domain Uptime"

When I receive the job I’ll get warnings for computers that failed the test, but the grid view will have my results.

Domain Uptime. (Image Credit: Jeff Hicks)
Domain Uptime. (Image Credit: Jeff Hicks)

 
Finally, note that there are no aliases or shortcuts in my script. I am using full cmdlet and parameter names. It doesn’t really take that much longer to type, especially if you take advantage of tab completion. In any event you only have to type it once. Between the help, inline comments, Write-Verbose commands and full command names, even someone totally new to PowerShell could read through the script and have an idea of what it is doing. I strongly encourage IT pros to write scripts and tools for the next person, because you might be that next person. You might come back to a script in six months and struggle through recalling what you wrote and why. Make your life, and that of those to follow you, easier.
There is one more stop I think we can make on our journey and we’ll get there next time.