Hey, Scripting Guy! Event 10 *Solutions* from Expert Commentators (Beginner and Advanced; the 1,500-meter race)


  

(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 _
       

Comments (0)

Skip to main content