Bookmark and Share 


(Note: These solutions were written for Beginner Event 2 of the 2010 Scripting Games
.)

 

Beginner Event 2 (Windows PowerShell)

Image of Christan Bellee

 


Christian Bellée—Microsoft Premier Field Engineer, Sydney, Australia.


I teach Windows PowerShell and VBScript courses for Microsoft Premier, and I am an Active Directory support engineer. I have also recently developed a Windows PowerShell 2.0 for the IT Administrator Workshop PLUS course for Microsoft Premier.


Get-RecentStartupEvent.ps1



Script:

#--------------------------------------------------------------------------------------------
#requires -version 2.0

param(
[array]$arrComputer
)

$arrEvent = @()

$arrComputer |
    ForEach-Object `
    {
       if (Test-Connection -ComputerName $_ -count 1 -Quiet) 
            {
            "`n Collecting event information from $_"
            $event = Get-EventLog -LogName system -ComputerName $_ |
            Where-Object {$_.eventID -eq 6005} |
            Select-Object -First 1
            $arrEvent += $event
            }    
        else {"`n $_ is Unreachable"}
    }
   
$arrEvent | Sort-Object -property TimeGenerated -desc |
Format-Table -Property MachineName,TimeGenerated,EventID,Message –autosize

#--------------------------------------------------------------------------------------------


Description


Collecting event log entries from remote machines is a very common administrative task, and Windows PowerShell 1.0 provided this functionality for local machines only, with the Get-Eventlog cmdlet. Although the underlying .NET Framework type, system.diagnostics.eventlog, used within the cmdlet has a constructor that allows us to specify the name of a remote machine, this wasn’t exposed in Windows PowerShell 1.0.


Windows PowerShell 2.0 enhanced the Get-Eventlog cmdlet to include the ability to specify a remote machine, making this script challenge a whole lot easier!


Essentially, the solution depends on the following simple Windows PowerShell pipeline command:


Get-EventLog -LogName system -ComputerName <computer name> | Where-Object {$_.eventID -eq 6005}


By employing the –ComputerName parameter of the Get-Eventlog cmdlet, we can target a remote machine. The resulting objects are then piped to a Where-Object cmdlet, which extracts all events of EventID 6005. Although, my script solution goes a bit further than this, allowing the user to pass an array of machine names into the script and sorting the entries from all the machines in descending date order.


The param() keyword allows a named argument to be specified on the command line and also casts the comma-separated string into an array, using the [array] type accelerator. Note the default machine name of “localhost” is assigned to the variable as a default.



param(
[array]$arrComputer=”localhost”
)


Next, we create an empty array to store our events for format processing later on:


$arrEvent = @()


The $arrComputer variable containing the list of remote computer names is then piped to a ForEach-Object cmdlet, where an if() statement containing the Test-Connection cmdlet determines whether the computer can be contacted. If the test fails, script execution moves to the else {} part of the statement and displays that the machine is unreachable. Determining whether remote machines are available before running further operations against them is vital when writing scripts for remote machines.



if (Test-Connection -ComputerName $_ -count 1 -Quiet)  {
<eventlog query code removed>
}
else {"$_ is Unreachable"}      


If the remote machine is responding, we can then query it for event log entries matching Event ID 6005 and select the most recent entry by using the Select-Object cmdlet and specifying the –First parameter as 1:


"Collecting event information from $_"
$event = Get-EventLog -LogName system -ComputerName $_ |
Where-Object {$_.eventID -eq 6005} |
 Select-Object -First 1


Next, the resulting event log entry is saved in the $event variable and added to the $arrEvents array we created at the beginning of the script:


$arrEvent += $event


Finally, we can sort the $arrEvents array of objects by their TimeGenerated property and format the result as an autosized table containing the MachineName, TimeGenerated, EventID, and Message properties of each object:


$arrEvent | Sort-Object -property TimeGenerated -desc |
Format-Table -Property MachineName,TimeGenerated,EventID,Message –autosize


To run the script, just pass a comma-separated list of machine names after the script name on the command line.


PC C:\> ./Get-RecentStartupEvent.ps1 machine1,machine2,machine3


The following image shows the results of the script.

Image of script output

 

 

Beginner Event 2 (VBScript)

Image of Stephanie Peters


Stephanie Peters, Senior Premier Field Engineer

Microsoft Solutions Support

 

This Is How I Roll


Before going any further, I should explain that I am known to wander outside the lines a bit. Being given a task like this one, which seems fairly simple, typically sends my mind into a maelstrom of “So how about if I…” or “I should make sure to add…” and almost always “But wouldn’t it be better if I…?” In the spirit of the games, I will make sure to meet the requirements technically, but I hope you’ll indulge my selfish habits a bit as I embellish and insert my own specifications into the mix.


Specify a Computer


Before we can get any information, we have to specify a target host. There are a number of ways to allow the user to specify a computer name. My favorite is the command-line argument. Personally I like to use named arguments, so I started off by accepting a named argument:


If Wscript.Arguments.Named.Count > 0 Then

      On Error Resume Next

            strComputer = WScript.Arguments.Named("Computer")

      On Error Goto 0

End If


Notice the On Error statements. I don’t typically like to run large sections of code under On Error Resume Next. I usually will identify a line where an error is likely to occur and only ignore errors for those specific lines. In this case we don’t need to check to see if an error occurred because our logic will test the value of strComputer later.


Since I’ve been using Windows PowerShell a lot the last few years, I’ve grown used to being able to provide an argument either as a named or a positional argument. I thought it might be nice to allow a positional argument (at the first position) if a named argument wasn’t specified:


If Not strComputer > "" Then

If WScript.Arguments.Count > 0 Then

            strComputer = WScript.Arguments(0)

      End If

End if


Finally, if no arguments were supplied to the script, we will fall back to a default value of “.”, which represents the localhost:

If Not strComputer > "" Then

      strComputer = "."

End If



Collecting Data


Because I’m going to potentially be doing time calculations at different points in my script, I decided to add a variable to hold the date time at a fixed point toward the beginning of the script, before any WMI values are returned:


dtmNow = Now


The next thing to do is to open a connection to the WMI Service on the target host. This is another time when an error could occur. The user could have specified an invalid computer name, there could be a permissions issue, or there could be a problem communicating with that computer over RPC. In this case, we need to handle any error by displaying a message to the user and aborting the script:


On Error Resume Next

      Set objWMIService = GetObject("winmgmts://" & strComputer _

   & "/root/cimv2")

      If Err.Number Then

            WScript.Echo "ERROR: There was a problem connecting to " _

               & the WMI service on """ & strComputer _

               & """.  The script is exiting."

            WScript.Quit

      End If

On Error Goto 0


When working with event logs via WMI, it’s best to scope your query so that it will return as little data as possible. Trust me on this one—I’ve had to restart too many WMI services over the years to steer you wrong on this. In this case, we’re looking for only events in the System Log whose source is “Eventlog” and whose Event ID is 6005. Further, we’re not returning all properties for these events because all we need to know is the TimeGenerated value.


strEventQuery = "SELECT TimeGenerated FROM Win32_NTLogEvent" _

   & " WHERE LogFile='System' AND SourceName = 'Eventlog' AND " _

   & " EventCode = '6005'"


Now we execute the query, which can also fail for a number of reasons (bogus query, WMI corruption on the target computer, more permissions issues, etc.). The thing to know about this is that most of the time it won’t fail on ExecQuery. It will usually fail when you try to loop through the collection it returns:


On Error Resume Next

      Set colEvents = objWMIService.ExecQuery(strEventQuery)

      For Each objEvent in colEvents

            If Err.Number Then

                  WScript.Echo "ERROR: There was a problem executing " _

   & "the query """ & strEventQuery _

                     & """ against host """ & strComputer _

                     & """.  The script is exiting."

                  WScript.Quit

            End If

            On Error Goto 0

            strUTCEventTime = objEvent.TimeGenerated

            Exit For   

      Next

On Error Goto 0


Notice above that we stored the TimeGenerated property value of the first object in the collection as the variable strUTCEventTime, and then we exited the For Each loop. The WMI Event Log provider always returns the most recent events first, which is lucky for us. This means we can just read the first event and forget the rest.

 

Ensuring a Good Result


Now we potentially have a date time value representing the last time the computer started. I say “potentially” because it’s likely that there was no such event in the System Log. The log might have been cleared or overwritten since the last time the computer started. In that case, our query would have succeeded, but we would have had no events in our collection to loop through. Therefore, strUTCEventTime would be empty.


First, let’s deal with the situation where we do have a date time value. WMI always returns dates and times in a UTC string format that is not so easy to read and really isn’t easy to leverage in a calculation. To overcome those challenges, we need to convert the UTC string to a DateTime value, for which we use the SWbemDateTime helper object:


If strUTCEventTime > "" Then

      Set objWMIDTHelper = CreateObject("WbemScripting.SWbemDateTime")

      objWMIDTHelper.Value = objEvent.TimeGenerated

      dtmEventTime = objWMIDTHelper.GetVarDate


While we’re at it, we can just go ahead and get the current uptime duration in seconds by comparing the date time we just got with the date time (dtmNow) we captured earlier in the script:


     
intUptimeSeconds = DateDiff("s", dtmEventTime, dtmNow)


Now, here’s where I go a bit rogue. If I wasn’t able to get a date time from an event, I know that I can still get the uptime information from a more reliable source. There is a performance counter (System\System Up Time) that is for this express purpose. In a perfect world with no fixed scenarios I would have just used this strategy and forgotten about event logs entirely:


Else

      Set objPerfOS = _

         objWMIService.Get("Win32_PerfFormattedData_PerfOS_System=@")

      intUptimeSeconds = objPerfOS.SystemUptime


After we have the uptime in seconds, we can easily calculate the date time of the last start. You’ll generally find that this will be at least several seconds before the Eventlog 6005 event:


     
dtmEventTime = DateAdd("s",-(intUpTimeSeconds),dtmNow)

End If



Making It Pretty


Rather than display the uptime duration in seconds, I decided to create a more readable string that shows the number of days, hours, minutes, and seconds that the computer has been up. At first I tried using a series of DateDiffs to break this out, but I found a nasty little bug that resulted in an extra minute and negative seconds. That’s when I decided to use Mod and the division operator to make this work. 


To understand how I did this, you have to go back to all those long division problems you did in elementary school. Given a value of 64088 seconds, I need to first figure out how many of those seconds are not accounted for in minutes.


64088 sec/60 sec = 1068 minutes with a remainder of 8 seconds

1068 min/60 min = 17 hours with a remainder of 48 minutes

17 hours/24 hours = 0 days with a remainder of 17 hours


What we would want to see is:  0 days, 17 hours, 48 minutes, and 8 seconds. We can achieve this by using the Mod operator, which gives usethe remainder of a division operation, subtracting the remainder from the original value and dividing the result by the specific interval to get to the next calculation. When we have all of the interval values, we can concatenate them into a readable string:


intTempValue = intUpTimeSeconds

intSeconds = intTempValue Mod 60

intTempValue = (intTempValue - intSeconds)/60

intMinutes = intTempValue Mod 60

intTempValue = (intTempValue - intMinutes)/60

intHours = intTempValue Mod 24

intTempValue = (intTempValue - intHours)/24

intDays = intTempValue

strUptime = intDays & " days, " & intHours & " hours, " & intMinutes & " minutes, " & intSeconds & " seconds"


All that’s left to do now is to echo the results to the user. I decided to go ahead and display both the date time of startup as well as the uptime duration.


WScript.Echo "Host """ & strComputer & """ has been running since " _

   & dtmEventTime

wscript.Echo "Uptime is " & strUptime

 


Let’s See It Work!

Image of script output

Note from Microsoft Scripting Guy Ed Wilson: Hey, did you know you can use Windows PowerShell to run a VBScript? Way cool trick, Stephanie! I love it!


The complete script is shown here.


GetComputerUptime.vbs

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'  GetComputerUptime.vbs                                                       '
'      Written by Stephanie Peters                                             '
'                                                                              '
'  This script returns the current uptime as determined by the last time the   '
'  event log service was started.                                              '
'                                                                              '
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

Dim colEvents
Dim dtmEventTime
Dim dtmNow
Dim intDays
Dim intHours
Dim intMinutes
Dim intSeconds
Dim intTempValue
Dim intUptimeSeconds
Dim objEvent
Dim objPerfOS
Dim objWMIDTHelper
Dim objWMIService
Dim strComputer
Dim strEventQuery
Dim strUptime
Dim strUTCEventTime

' Provide an input mechanism for the computer name with the following order
' of precendence:  Named argument, positional argument, default value of
' localhost (.)

If Wscript.Arguments.Named.Count > 0 Then
      On Error Resume Next
            strComputer = WScript.Arguments.Named("Computer")
      On Error Goto 0
End If

If Not strComputer > "" Then
      If WScript.Arguments.Count > 0 Then
            strComputer = WScript.Arguments(0)
      End If
End if

If Not strComputer > "" Then
      strComputer = "."
End If

' Echo an empty line for looks
WScript.Echo
' Get the current time to use as a reference point for calculating uptime
dtmNow = Now

' Get a reference to the WMI service on the target computer
On Error Resume Next
      Set objWMIService = GetObject("winmgmts://" & strComputer & "/root/cimv2")
      If Err.Number Then
            WScript.Echo "ERROR: There was a problem connecting to the WMI service on """ _
               & strComputer & """.  The script is exiting."
            WScript.Quit
      End If
On Error Goto 0

' Construct the query string
strEventQuery = "SELECT TimeGenerated FROM Win32_NTLogEvent" & _
   " WHERE LogFile='System' AND SourceName = 'Eventlog' AND EventCode = '6005'"

' Execute query and return results
On Error Resume Next
      Set colEvents = objWMIService.ExecQuery(strEventQuery)
      For Each objEvent in colEvents
            If Err.Number Then
                  WScript.Echo "ERROR: There was a problem executing the query """ _
                     & strEventQuery & """ against host """ & strComputer _
                     & """.  The script is exiting."
                  WScript.Quit
            End If
            On Error Goto 0

            ' We only need to store the TimeGenerated property of the most recent event.       
            strUTCEventTime = objEvent.TimeGenerated
            ' The WMI Event Log Provider always returns the most recent events first.
                ' After we've read one item, we can skip the rest.
            Exit For   
      Next
On Error Goto 0

If strUTCEventTime > "" Then
      ' Since WMI returns a UTC date time string, we have to convert it to a DateTime subtype
      Set objWMIDTHelper = CreateObject("WbemScripting.SWbemDateTime")
      objWMIDTHelper.Value = objEvent.TimeGenerated
      dtmEventTime = objWMIDTHelper.GetVarDate
      ' Given the DateTime of the last start up, we can calculate the number of seconds
      ' of uptime.
      intUptimeSeconds = DateDiff("s", dtmEventTime, dtmNow)
Else
      ' If there were no matching events, the variable strUTCEventTime would be empty.
      ' This is a common scenario because event logs can be cleared or overwritten after a
      ' system was started.
      ' In this case, we'll get the uptime seconds from the performance counter designed
      ' for this purpose.
      Set objPerfOS = objWMIService.Get("Win32_PerfFormattedData_PerfOS_System=@")
      intUptimeSeconds = objPerfOS.SystemUptime
      ' Given the number of seconds of uptime, we can calculate the DateTime of the last
      ' startup.
      dtmEventTime = DateAdd("s",-(intUpTimeSeconds),dtmNow)
End If

intTempValue = intUpTimeSeconds
intSeconds = intTempValue Mod 60
intTempValue = (intTempValue - intSeconds)/60
intMinutes = intTempValue Mod 60
intTempValue = (intTempValue - intMinutes)/60
intHours = intTempValue Mod 24
intTempValue = (intTempValue - intHours)/24
intDays = intTempValue

strUptime = intDays & " days, " & intHours & " hours, " & intMinutes & " minutes, " & intSeconds & " seconds"

WScript.Echo "Host """ & strComputer & """ has been running since " & dtmEventTime
wscript.Echo "Uptime is " & strUptime
WScript.Echo

 


If you want to know exactly what we will be looking at tomorrow, follow us on
Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys