(Note: These solutions were written for Event 10.) 

Beginner Event 10:  The 1,500-meter race

In the 1,500-meter race, you will go for the gold by writing a script that counts down from three minutes.

Guest commentator: Andrew Willett

Image of guest commentator Andrew Willett 

Andrew Willett is the projects manager for the IT department of a Steinhoff group company. Based in London, he spends most of his time deploying, designing, and developing Microsoft-based architectures.

VBScript solution

I wrote the solution to the Beginner 1,500-meter race event using probably the most ubiquitous developer environment in the world: Notepad!

To perform the actual counting down by a second, I used a While loop containing a call to WScript.Sleep().  To make the script sleep for 1 second, we pass it 1,000 milliseconds as a parameter, and decrementing the seconds remaining by 1 each time.

When run from the console using cscript.exe, the script prints each decrement to the console when counting down to zero. When run interactively using Windows Scripting Host, this is suppressed (it would normally manifest itself as a blocking, modal dialog box) and only notifies the user on start and finish.

The script allows the user to change the default duration of 180 seconds to any other integer value by the use of a simple command-line argument, in the form BeginnerEvent10Solution.vbs <int>. The script performs basic error-checking to validate the value before using it in place of the default. If no command-line argument is passed and the default is used, command-line users see a reminder of the syntax required for passing the argument.

When the script is run, this output is displayed:

Image of the output when script is run


The complete BeginnerEvent10Solution.vbs script is seen here.

BeginnerEvent10Solution.vbs

'The time remaining/duration of the script.
Dim remaining

'Whether the script is running interactively.
Dim nonInteractive

'Initial duration of 180 seconds.
remaining = 180

'Initially presume the script is running interactively.
nonInteractive = false

'If the script is running non-interactively through the console.
If "cscript.exe" = LCase(Right(WScript.FullName, 11)) Then

    'The script is running non-interactively.
    nonInteractive = true

End If

'Check if a command-line argument was passed.
If (WScript.Arguments.Length <> 0) Then

    'See if the first command-line argument is an *integer*.
    On Error Resume Next
    i = CInt(WScript.Arguments.Item(0))
    If (Err.Number = 0) And (WScript.Arguments.Item(0) = CStr(i)) Then

        'Set the duration to that of the command-line argument.
        remaining = WScript.Arguments.Item(0)

    Else

        'Inform the user that the argument was not valid, and quit.
        WScript.Echo("Error: The command-line argument passed was not understood.")
          WScript.Quit

    End If
    On Error GoTo 0

Else

    'If the script is running from the console
    If (nonInteractive) Then

        'Suggest that command-line arguments can be used.
        WScript.Echo("To change the duration, use script.vbs <duration-in-seconds>")
        WScript.Echo("")

    End If

End If

'Read-out the summary.
WScript.Echo("The script will now count-down from " & remaining & " seconds.")

'If the script is running from the console
If (nonInteractive) Then

    'Blank line to keep the console looking tidy.
    WScript.Echo("")

End If

'While there is still time remaining.
While remaining > 0

    'If script is running interactively, supress the 'ticking' as it blocks, otherwise:
    If (nonInteractive) Then

        'Echo the number of seconds remaining.
        WScript.Echo(CStr(remaining) & " seconds remain")

    End If

    'Decrement the time remaining by one.
    remaining = remaining - 1

    'Pause for 1000 milliseconds (1 second).
    WScript.Sleep(1000)

WEnd

'If the script is running from the console
If (nonInteractive) Then

          'Blank line to keep the console looking tidy.
          WScript.Echo("")

End If

'Echo that the time has elapsed.
WScript.Echo("> time elapsed <")
 


Guest commentator: Thomas Lee

Image of guest commentator Thomas Lee

Thomas Lee has been scripting pretty much forever. He was pretty hot at JCL on the IBM 360 in the late 1960s, and did a ton of shell scripting in the 70s on ICL VME. He learned batch scripting with DOS 2.0 but managed to avoid VBScript. After he saw the beta of Windows PowerShell in September 2003, he never looked back. Thomas is proficient in both Windows PowerShell 2.0 and 1.0, and he specializes in both the .NET and WMI aspects of the language. Thomas has the distinction of being the first person to blog about Windows PowerShell. He is also a moderator on the Hey, Scripting Guy! Forum and maintains the Under the Stairs blog.

Windows PowerShell solution

This year for the Scripting Games I was asked to write the solution for Beginner Event 10, the 1,500-meter race. This script is a countdown timer that goes from three minutes to zero seconds. When the time is up, it displays a message indicating that the given amount of time has elapsed. That sounded fairly simple so I set to work.

I saw this as really being two separate things to do. These two tasks are listed here:

· Construct some sort of loop that displays the current time remaining, wait a second, and then do it again.

· Display the number of seconds in a nice way.

The basic outline of the script is pretty easy. I use the Start-Sleep cmdlet and wait for a second. The basic script is seen here:

$time = 180
do {display $time; start-sleep 1; $time--} Until $time=0
"All Done"

That is all fine and well, but there is only one small problem. If the script sleeps for exactly 1 second, the total time for each iteration is 1 second plus however long it takes to do the time display, etc. In other words, the whole script will run longer than 3 minutes. The time between each call to the display function would be a little more than 1 second.  To get around this, I added in a fudge factorsome number of milliseconds I would deduct to account for the additional activities. Thus, I'd go to sleep for 1 second less the fudge factor. In the script that I use, I've set the fudge factor to 5 milliseconds. You can see at the end of the script the actual time used. When you run this script on your own machine, you can adjust the script to suit you.

The second problem is how to display the number of seconds nicely. To separate the display aspects from the rest of the script, I created a function that takes the time left (in seconds) and displays it nicely.  That meant I could get the basic script working, and then work out how to make the output look better.

To get the script working, I initially just cleared the screen, and displayed the number of seconds available.  Pretty boring but it was progress! Next I got the idea of leveraging the System.TimeSpan object. I created a new object from the total number of seconds remaining. The timespan object converts the total number of seconds into the minutes and seconds that are left. That made the display almost complete. I then decided to display "minute" when the number of minutes left was one instead of "1 minutes". And I did the same thing for number of seconds.

When the script runs it clears the screen and displays the time remaining. This is seen here:

Image of results of running script


When the BeginnerEvent10Solution.ps1 script has completed the counting, it displays the final time the script really took. This is seen here:

Image of final time the script took

As you can see, the total time was a tad more than 3 minutes, but that is close enough for our purposes.

So there's a basic timer script. If I had more skills with System.Forms, I might have been able to create a nicer bit of output. But I'll leave that as an exercise for the more skilled!

There are a number of solutions for easily creating Windows Forms from within Windows PowerShell. One such solution is Primal Forms from SAPIEN. Another is PowerBoots, which is a CodePlex project. Another one is the presentation framework being developed by James Brundage.

The completed BeginnerEvent10Solution.ps1 script is seen here.  This script requires Windows PowerShell 2.0.

BegginerEvent10Solution.ps1

<#
.SYNOPSIS
    This script counts down from 3 minutes and displays the time remaining.
.DESCRIPTION
    This script is an entry in the 2009 Summer Scripting Games.
.NOTES
    File Name  : Display-Counter.ps1
          Author     : Thomas Lee - tfl@psp.co.uk
          Requires   : PowerShell V2 CTP3
.LINK
    This script posted to:
              http://www.pshscripts.blogspot.com
.EXAMPLE
    Left as an exercise for the reader.
#Requires –version 2.0
#>

# First helper function to display the time remaining
function display-time {
param ($timetodisplay)
# clear the screen
cls

# now create a timespan object from number of seconds
$display = New-Object System.TimeSpan 0,0,$timetodisplay

# Get minutes and seconds
$min=$display.minutes
$sec=$display.seconds

# Now work out "second" vs "seconds" and minutes
if ($min -gt 1 -or $min -eq 0){$mintag="Minutes"}
          else {$mintag="Minute"}
if ($sec -gt 1 -or $sec -eq 0) {$sectag="seconds"}
                          else {$sectag="second"}

# now print out minute(s) and second(s) remaining
"{0} {1}, {2} {3}" -f $min,$mintag,$sec,$sectag
}

# start of script

#define time in seconds (3 minutes or 180 seconds)
$time = 180 

# define fudgefactor - number of milliseconds to wait to avoid
# timing errors in start-sleep etc.
$fudgefactor = 5
$interval    = 1000 - $fudgefactor

# start time
$starttime=Get-Date
do {
display-time $time

Start-Sleep -Milliseconds $interval
$time--
} while ($time -gt 0)
cls
"Done - counted down to $time seconds"

# Now calculate how long it really took
$finishtime=Get-Date
"Script took this long:"
$finishtime-$starttime
# end of script

Advanced Event 10: The 1,500-meter race

In the 1,500-meter race event, your script will need to be able to go the distance as you dynamically change the priority of a particular process every time the process is launched.

Guest commentator: Eric Lippert

Image of guest commentator Eric Lippert

Eric Lippert is in the Developer Division at Microsoft. He was on the team that designed and implemented new features for VBScript, Jscript, and Windows Script Host from 1996 through 2001. After a few years working on Tools for Office, he now works on the C# compiler. He maintains the Fabulous Adventures In Coding blog on MSDN that is mostly about C# these days, but there is a large archive of articles about the design of VBScript and Jscript in there. This makes fascinating reading if you’re interested in that sort of thing.

VBScript solution

There’s an odd thing that you learn when working on developer tools: The people who design and build the tools are often not the experts on the actual real-world use of those tools. I could tell you anything you want to know about the VBScript parser or the code generator or the runtime library, but I’m no expert on writing actual scripts that solve real problems. This is why I was both intrigued and a bit worried when the Scripting Guys approached me and asked if I’d like to be a guest commentator for the 2009 Summer Scripting Games.

I wrote this script the same way most scripters approach a technical problem that they don’t immediately know how to solve; I searched the Internet for keywords from the problem domain to see what I could come up with. Of course, I already knew about our MSDN documentation, I had a (very) basic understanding of WMI, and I knew that the Scripting Guys had a massive repository of handy scripts.

My initial naïve thought was that I would have to go with a polling solution; sit there in a loop, querying the process table every couple of seconds, waiting for new processes to pop up. Fortunately, my Internet searches quickly led me to discover that process startup events can be treated as an endless collection of objects returned by a WMI query.

That got me thinking about the powerful isomorphism between events and collections.

A collection typically uses a “pull” modelthe consumer asks for each item in the collection one at a time as needed, and the call returns when the item is available. Events typically work on a “push” modelthe consumer registers a method that gets called every time the event fires. But not necessarily; the WMI provider implements events on a “pull” model. The event is realized as a collection of “event objects.” It can be queried like any other collection. Asking for the next event object that matches the query simply blocks until it is available.

Similarly, collections could be implemented on a “push” model. They could call a method whenever the next item in the collection becomes available. The next version of the CLR framework is likely to have standard interfaces that represent “observable collections”, that is, collections that “push” data to you, like events do. The ability to treat events as collections and collections as events can lead to some interesting and powerful coding patterns.

I seem to have digressed somewhat from the topic at hand.

The code is straightforward. We begin by checking the validity of the command-line arguments, and then wait for new processes that match by name to be created. When one is created, we look it up in the process table to get its process object, and then set the priority of the process to the appropriate level.

One interesting point about the AdvancedEvent10Solution.vbs script is that it avoids an unlikely but nevertheless possible bug. In the microseconds after the creation of the new process but before we set its priority, it is possible that the original process ends and a new process with a different name begins. If the operating system is running low on unique process IDs, it is possible that the one that just freed up could be re-used. Therefore I ensure that the process that gets its priority set matches in both process ID and name. That way, we ensure that we never set the priority of the wrong process.

I deliberately omitted error handling code, except for checking whether the query result was null. There are a number of situations where the script could fail. For example, setting a process priority to RealTime is a dangerous operation that is typically restricted to administrators; a badly behaved process with such high priority can “starve” important processes, preventing them from ever getting any processor time. If an attempt to set priority fails, the AdvancedEvent10Solution.vbs script will simply crash. Arguably, that is the safer thing to do rather than to attempt to recover from the situation and continue. An alternative approach would be to detect the failure and attempt to “do the best we can” by setting the priority to High should an attempt to set to RealTime fail. Because error handling was not in the specification of the problem, that’s not what I implemented.

The complete AdvancedEvent10Solution.vbs script is seen here.

AdvancedEvent10Solution.vbs

Option Explicit

Const IdlePriority         = &h0040&
Const BelowNormalPriority  = &h4000&
Const NormalPriority       = &h0020&
Const AboveNormalPriority  = &h8000&
Const HighPriority         = &h0080&
Const RealTimePriority     = &h0100&

Dim ProcessName
Dim PriorityName
Dim Priority

Main

Sub Main()
    CheckArguments
    SetPriority
End Sub

Sub CheckArguments()
    Dim Dictionary
   
    If WScript.Arguments.Count <> 2 Then
        ShowUsage
        WScript.Quit
    End If
   
    Set Dictionary = CreateObject("Scripting.Dictionary")
    Dictionary.CompareMode = vbTextCompare
    Dictionary.Add "Idle", IdlePriority
    Dictionary.Add "BelowNormal", BelowNormalPriority
    Dictionary.Add "Normal", NormalPriority
    Dictionary.Add "AboveNormal", AboveNormalPriority
    Dictionary.Add "High", HighPriority
    Dictionary.Add "RealTime", RealTimePriority
   
    ProcessName = WScript.Arguments(0)
    PriorityName = WScript.Arguments(1)
    If Not Dictionary.Exists(PriorityName) Then
        ShowUsage
        WScript.Quit
    End If
   
    Priority = Dictionary(PriorityName)
   
End Sub

Sub ShowUsage()
    WScript.Echo _
        "Usage: " & WScript.ScriptName & " process.exe priority " & vbCrLf & _
        "priority: one of Idle, BelowNormal, Normal, AboveNormal, High, RealTime" & vbCrLf
End Sub

Sub SetPriority()

    Dim WMI, Events, Process, Processes, ProcessStartTrace
   
    Set WMI = GetObject("winmgmts:")
  
    Set Events = WMI.ExecNotificationQuery _
        ("Select * From Win32_ProcessStartTrace Where ProcessName = '" & ProcessName & "'")
 
    Do While(True)
        WScript.Echo "Waiting for " & ProcessName & " to start"
        Set ProcessStartTrace = Events.NextEvent

        ' This avoids the race condition where the process shuts down
        ' and a new process with the same process ID but different name
        ' starts up
        
        Set Processes = WMI.ExecQuery _
            ("Select * From Win32_Process Where ProcessId = " & ProcessStartTrace.ProcessId & _
             " And Name = '" & ProcessStartTrace.ProcessName & "'")
        If Not (Processes Is Nothing) Then
            For Each Process in Processes
                WScript.Echo "Setting priority of " & ProcessName & " to " & PriorityName
                Process.SetPriority Priority
            Next
        End If
    Loop

End Sub
 

Guest commentator: Don Jones

Image of guest commentator Don Jones

Don Jones has more than a decade of professional experience in the IT industry. He’s the author of more than 30 IT books, including Windows PowerShell: TFM; VBScript, WMI, and ADSI Unleashed; Managing Windows with VBScript and WMI; and many more. He’s a top-rated and in-demand speaker at conferences such as Microsoft TechEd and TechMentor, and writes the monthly Windows PowerShell column for Microsoft TechNet Magazine. Don is a multiple-year recipient of Microsoft’s Most Valuable Professional (MVP) Award with a specialization in Windows PowerShell. Don’s broad IT experience includes work in the financial, telecommunications, software, manufacturing, consulting, training, and retail industries and he’s one of the rare IT pros who can not only “cross the line” between administration and software development, but also between IT workers and IT management. He is a co-founder of Concentrated Technology where he blogs nearly every day about Microsoft-related technologies, including Windows PowerShell.

Windows PowerShell solution

PowerShell architect Jeffrey Snover set me on the right path to this one with his blog post about trapping WMI events. I decided to split up the tasks: One function watches for WMI events and outputs them to the pipeline, and a second function receives information about events that have happened. This split-brain approach makes it easier to repurpose the event-watching function later. After an event occurs, I check to see if it’s the desired process name (passed via an input parameter to the function), and if it is, I set its priority. As shown in the image below, the script tells you what it’s doing and you can see that the priority of the process (in Task Manager) has indeed been changed. Notice that I used Write-Host to write output messages; you could substitute Write-Verbose, and let the shell-wide $VerbosePreference variable dictate whether or not any output was shown. I worry about forgetting which key to press to cancel it, though!

You could take this script a bit further to add flexibility: The process name input parameter, for example, could be treated as an array, allowing a comma-delimited list of process names to be passed to the function. A downside of the script is that, in Windows PowerShell 1.0, the only way to catch and respond to WMI events is to run in a continuous loop. In Windows PowerShell 2.0, this script could be rewritten to leverage new event-response features, allowing you to continue working in the shell while waiting for events to occur.

The completed AdvancedEvent10Solution.ps1 script is seen here.

AdvancedEvent10Solution.ps1

function New-ProcessStartWatcher {

          # constants for key codes
          $ESCkey = 27
         
          # warning message
          Write-Host "Started watching for new processes; press ESC to quit" -foreground Green -background Black

          # start WMI event sink
          $query = New-Object system.Management.WqlEventQuery 'Select * from Win32_ProcessStartTrace'
          $scope = New-Object system.Management.ManagementScope 'root\cimv2'
          $watcher = New-Object system.Management.ManagementEventWatcher $scope,$query
          $options = New-Object system.Management.EventWatcherOptions
          $options.TimeOut = [timespan]'0.0:0:1'
          $watcher.Options = $options
          $watcher.Start()

          # wait for event
          while ($true) {
                   trap [System.Management.ManagementException] {continue}
                   $watcher.WaitForNextEvent()
                  
                   # watch for ESC or Q keypress to abort
                   if ($Host.UI.RawUi.KeyAvailable) {
                             $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp')
                             if ($key.VirtualKeyCode -eq $ESCkey) {
                                      $watcher.stop()
                                      break
                             }
                   }
          }

}

function Set-ProcessPriority {
          param ( [string]$processname = 'notepad.exe',
          [string]$priority = 'normal'
          )
          BEGIN {
                   # translate priority string to the numeric value needed
                   # by WMI
                   $prioritynumber = 0
                   switch ($priority) {
                             'idle' { $prioritynumber = 64; break }
                             'belownormal' { $prioritynumber = 16384; break }
                             'normal' { $prioritynumber = 32; break }
                             'abovenormal' { $prioritynumber = 32768; break }
                             'highpriority' { $prioritynumber = 128; break }
                             'realtime' { $prioritynumber = 256; break }
                   }
          }
          PROCESS {
                   if ($_.processname -eq $processname) {
                             $process = gwmi Win32_Process -filter "Name='$processname'"
                             Write-Host "Setting $processname to $priority" -foreground green -background black
                             $process.SetPriority($prioritynumber) | Out-Null
                            
                   }
          }
}

new-processstartwatcher | set-processpriority 'notepad.exe' 'idle'

When the AdvancedEvent10Solution.ps1 script is run, this output is displayed:

Image of output displayed when script is run

 

So…this brings us to the end of another round of great commentaries for the 2009 Summer Scripting Games. This also brings us to the end of this year’s Scripting Games. Stay tuned next week to see what we come up with as Craig and I try to catch our breath! It will be cool; we can at least say that. If you would like to catch all the latest news, follow us on Twitter. If you want to be really cool, you can join the The Scripting Guys group on Facebook.

Ed Wilson and Craig Liebendorfer, Scripting Guys