How to Script In-Your-Face Alerts
A TSA notification system
February 7, 2005
Ensuring that machines are online is one of the most important tasks that network administrators perform every day. The moment a machine goes offline, administrators need to be alerted so that they can get the machine back online. With time being such a crucial factor in high network availability, administrators need a monitoring tool that can quickly notify them when a machine is offline.
Perl is the perfect scripting language for writing such a tool. However, although writing a Perl script to monitor machines is simple, scripting a notification system isn't so simple.
To be fair, you can easily write a monitoring script that produces a log file, registers Win32 event log entries, or even sends an email message or a page. But if you're working on a computer when something goes wrong, you need an alert that jumps out and is in your face. You could decide to write code that pops open a dialog box with a message. But if you use that approach and many alerts occur while you're away from your computer, you have to wade through numerous dialog boxes and click OK in each one. A better way to provide alerts is to use the taskbar status area (TSA).
The TSA is the region of the Windows taskbar (typically at the lower right of your Windows display), where icons appear alongside the system clock. This area provides the user with information about what an application is doing. For example, when Windows Update has downloaded a new system patch, an icon appears that informs the user a patch is ready to be installed.
Applications have been using the TSA more and more, but Perl scripts haven't been able to take advantage of it because there's no native support for registering an icon with the TSA (although ActiveState offers a tool that can compile a Perl script into a TSA application called PerlTray). However, you can add such support. Let's explore how the TSA works and how to add TSA support to a machine-monitoring script.
How the TSA Works
When an application registers an icon with the TSA, the application's icon is displayed alongside the other icons. An application can add as many icons as it needs, but most applications display only one icon. After an icon is registered, it remains visible until the application deletes the icon or is terminated. If the application fails to delete the icon before termination, the icon will remain visible until the user moves the mouse pointer over it.
As situations change, an application can update its TSA icon to reflect changing conditions. For example, when an application is running smoothly, its icon might be a green light, but when the application encounters a problem, the icon might change to a red light. After the problem is corrected, the application icon can change back to the green light. This iconic visual representation of an application's status is a quick and easy way for the user to learn about problems that need attention.
In addition to an icon, an application can add a tool tip. The tool tip contains text that's associated with the application's icon. The text is usually a message that provides status details about the application's state. To continue with the previous example, when the icon turns to a red light, the tool tip might read Low disk space or Unable to process request. Users see this tool tip when they pause the mouse pointer over the icon. The tool tip appears above the icon. For example, Figure 1 shows the tool tip for the smiley face icon.
The application can change the tool-tip text at any time. Thus, the tool tip can provide a concise description of what's currently happening, without users having to open a log file and read an entire log or open a window and scroll through outputted log data. Icon changes and tool-tip updates are great feedback tools an application can employ to inform users of changes in status.
However, when an important event takes place, an application might need to do more than just update text and change an icon. The application might need to capture users' attention so that they know to look at the icon or tool tip. If users are deeply engrossed in writing a business proposal, updating a budget spreadsheet, or trying to win a hand of solitaire, they might be blissfully ignorant of a changed icon or updated tool tip.
The TSA has the ability to pop up an alert window that's guaranteed to get users' attention. This balloon-like window is large enough to hold a title and detailed description of a problem. In addition, the alert can include an icon that denotes the seriousness of the problem. For example, Figure 2 shows an alert that uses an error icon (in Windows XP, this icon is a red circle with a white X) to indicate a high severity level. Other possible icons include an exclamation point and a question mark.
Adding TSA Support to a Script
Now that you know how TSA works, let's explore how to add TSA support to a machine-monitoring script. This script, HostMonitor.pl, serves two purposes. First, it provides a working script that you can use to monitor remote machines on a network. It notifies you when one or more of the machines are unreachable. This script is useful for anyone who needs to monitor machines. Second, HostMonitor demonstrates how a Perl script can interact with the TSA. This knowledge is useful if you have scripts that would benefit from such TSA interaction.
You can download HostMonitor from the Windows Scripting Solutions Web site. Go to http://www.windowsitpro.com/windowsscripting, enter 45195 in the InstantDoc ID text box, then click the 45195.zip hotlink. The 45195.zip file includes HostMonitor_Files.zip, which contains the script and two icon files.
I wrote HostMonitor to work on XP, but it should also work on other Windows OSs. The script requires three Perl packages that don't come with all versions of Perl. The packages are
Win32::API—Although HostMonitor doesn't explicitly load Win32::API, Win32::API::Prototype requires Win32::API. Use the Perl Package Manager (PPM) to install Win32::API. Run the command
ppm install win32-api
Win32::API::Prototype— Win32::API::Prototype makes using Win32::API much easier. Use PPM to install Win32::API::Prototype. Run the command
ppm install http://www.roth.net/perl/ packages /win32-api-prototype.ppd
(Although this command appears on several lines here, you would enter it on one line. The same holds true for the other multiline commands in this article.)
Win32::PingICMP—Win32::PingICMP is a Win32-specific ping module. Run the command
ppm install win32-pingicmp
HostMonitor is easy to use. To launch the script, you specify the IP addresses or names of the machines you want to monitor. For example, if you want to specify the machines by their IP addresses, you might use the command
Perl HostMonitor.pl 192.168.1.1 192.168.1.87
If you want to specify the machines by their names, the command might look like
Perl HostMonitor.pl \Server1 \Machine2
You can run the script against any number of machines.
Along with the machine IP addresses and names, you can include several optional parameters:
-m MAX_COUNT, where MAX_COUNT is the number of failed pings allowed before the script considers a machine unreachable. If you don't set the -m parameter, the script considers a machine unreachable after 5 failed pings. Whether you want to change this number will depend on your traffic levels, network architecture, and how busy your machines are.
-i SECONDS, where SECONDS is the number of seconds between ping attempts. The shorter the interval, the quicker you'll be alerted to problems. However, shorter intervals also means more traffic is generated. If you don't set the -i parameter, the script waits 10 seconds between ping attempts.
-t SECONDS, where SECONDS is the number of seconds to wait for a ping response. When this number is reached, the script gives up and decides that a ping attempt has failed. If you don't set the -t parameter, the script waits 1 second for a ping response. For small closed networks, 1 second is fine. But when targeting a machine across the Internet or over slow modem connections, a higher value is justified.
You can place the optional parameters before or after the machine IP addresses or names. For example, suppose you want to monitor a machine that's nearby and that always provides reliable ping responses. You might use the command
Perl HostMonitor.pl router -t 1 -m 1
The parameters apply to all the specified machines.
How the Script Works
HostMonitor has to do quite a bit of preparation before it can ping the remote machines. It needs to create some constants, load some functions, load and add the icons, and create an important global object.
Listing 1 shows the code that creates the constants and loads three Win32::API::Prototype functions. The code at callout A in Listing 1 defines a series of constant values. This code begins by populating the %CONSTANTS hash with names and values of constants used throughout the script. After the hash is created, the code walks through each name and value and creates an anonymous code segment that returns the constant's value. The code then assigns the code segment to a glob that has the same name as the constant.
For those of you who aren't familiar with this scripting technique, it's a cheater's way to create a constant (although it's probably the most commonly accepted way to create a constant in Perl). Perl doesn't have true support for constant values, so you either have to create a variable that contains the value (which can be accidentally modified) or create a subroutine that returns the desired value. HostMonitor uses the latter approach but does so dynamically rather than manually coding a subroutine for each constant. The benefit of dynamically generating constant subroutines is that it's easy to add constants to or remove them from the %CONSTANTS hash.
The code at callout B in Listing 1 calls the Win32::API::Prototype module's ApiLink method. This method provides an easy way to interact with Win32::API and load DLLs so that the script can call the library's functions. The three functions being exposed in this code block are used throughout the script.
Listing 2 shows the code that loads and adds the icons to the TSA and creates an important global object. To add an icon to the TSA, you must first load it into memory. The code at callout A in Listing 2 does just that. It examines the directory in which HostMonitor is located and loads all icon files that match the $Config{icon_path_mask} mask. By default, the icon files are HostMonitor_Normal.ico and HostMonitor_Error.ico, which you can find in the HostMonitor_Files.zip file. HostMonitor_Normal.ico provides a smiley face icon to denote a normal state. HostMonitor_Error.ico provides an exclamation-point icon to denote an error state. Windows' LoadImage function loads each icon. The function returns a numeric value known as an icon handle, which is used to identify the icon. The handle is added to the %IconList hash.
The code at callout A also creates the %IconData hash, which is used throughout the script to hold data that will be applied to the TSA icon. There's one notable key in this hash: the window key. This key is assigned the value returned from a call to Windows' CreateWindowEx function. This function creates a Ghost class window that's invisible and used solely for receiving messages from other processes. The value returned by CreateWindowEx is the window handle. This handle is used to create the TSA icon later in the script.
Callout B in Listing 2 highlights the code that adds an icon to the TSA. First, the code tells the TSA to allocate a location for an icon by calling the CreateIcon() subroutine. Next, the code updates the TSA with an icon (in this case, the smiley face icon), then calls the UpdateNormalStatus() subroutine to add a tool tip. The last line in callout B traps the script's interrupt signal ($SIG{'INT'}). Without this line, if the script were to terminate before it could tell the TSA to remove the icon, the icon would continue to show as if the script were still running. Only by moving your mouse over the orphaned icon would Windows discover that the icon needs to be removed. To prevent this situation, the script traps the interrupt signal. That way, if you use Ctrl+C to stop the script, the TerminateScript() subroutine will remove the TSA icon. However, if the script terminates abruptly (e.g., it's killed by the Task Manager or the die command), the icon will remain in the TSA until you manually click it with a mouse.
Callout C shows the last block of setup code that runs before the script starts monitoring machines. This code creates a global Win32::PingICMP object ($PingObject). The script will use this one object to ping all the machines. This script makes use of Win32::PingICMP instead of the more common Net::Ping module because Net::Ping works only on Win32 machines if the script is run under an administrator account.
With the preparatory work finished, the script can finally start pinging machines. The script repeatedly calls various subroutines to ping machines and check whether the pings are successful. The first of these subroutines is PingHosts(), which Listing 3 shows. This simple subroutine enumerates each machine's name. These names are exposed as keys of the passed-in $HostList hash reference. For each machine, PingHosts() calls the global $PingObject's ping method, which callout A in Listing 3 shows. If the first ping is unsuccessful, the subroutine decrements the machine's ping count by 1 and records the date and time. The script later uses the date and time information to determine how long a machine has been unavailable. If a subsequent ping is unsuccessful, the subroutine again decrements the machine's ping count by 1 but doesn't record the date or time. When the ping is successful, the subroutine resets the ping count.
After the PingHosts() subroutine runs, the script calls the CheckHosts() subroutine. CheckHosts() first checks to see whether each machine's ping count has reached 0. When the ping count is 0, the subroutine updates the $AlertMessage and $Message strings with details about the unreachable machine. It also increments the $iDownCount variable's value by 1.
The $iDownCount variable tracks how many machines are currently unavailable. When $iDownCount isn't 0, there's at least one unavailable machine. In response, CheckHosts() raises an alert by calling the Alert() subroutine. When $iDownCount is 0, all hosts are available and the subroutine checks to see whether an alert is still shown. If so, it clears the alert. Otherwise, CheckHosts() calls the UpdateNormalStatus() subroutine to update the TSA icon's tool tip.
As I just mentioned, the Alert() subroutine raises an alert. The TSA has limitations to how much data can be displayed, so Alert() first truncates the alert title, the alert message, and the tool-tip text. Next, the subroutine sets a flag so that the script knows that it's currently displaying an alert. Then, the subroutine updates the %IconData hash with information. Finally, Alert() calls the UpdateTSAIcon() subroutine.
Similarly, the ClearAlert(), ChangeIcon(), and ChangeText() subroutines update the %IconData hash with information, then call the UpdateTSAIcon() subroutine. Note that each call to UpdateTSAIcon() passes in a flag. This flag indicates what information from the %IconData hash will be used.
Listing 4 shows UpdateTSAIcon(). This subroutine is where all the TSA magic takes place. Anything that the script does regarding the TSA goes through this subroutine. UpdateTSAIcon() begins by creating a packed $pNotifyIconData scalar variable. This variable uses the NOTIFYICONDATA constant as the packing template, which creates a NOTIFYICONDATA data structure just like a C or C++ program would. This data structure contains information that the system needs to process TSA messages.
A NOTIFYICONDATA structure contains specific members. The first member is the size (in bytes) of the data structure. (You can learn about the other members at http://msdn.microsoft.com/library/enus/shellcc/platform/shell/reference/structures/notifyicondata.asp.) To determine how big the structure will be, UpdateTSAIcon() packs the data structure with nothing but 0s, as callout A in Listing 4 shows. After the subroutine determines the size, it fills the data structure with actual values from the %IconData hash, as callout B in Listing 4 shows.
After $pNotifyIconData is packed with actual values, UpdateTSAIcon() passes it to Windows' Shell_NotifyIcon function. This function creates, modifies, and deletes icons from the TSA. Details about this function are available at http://msdn.microsoft.com/library/enus/shellcc/platform/shell/reference/functions/shell_notifyicon.asp.
The final subroutine worth noting is the TerminateScript() subroutine. HostMonitor calls this subroutine when you use Ctrl+C to terminate the script. TerminateScript() calls the RemoveIcon() subroutine, which in turn calls UpdateTSAIcon(). UpdateTSAIcon() then removes the script's icon from the TSA.
The TSA Is a Scripter's Friend
Scripters underutilize the TSA. Considering the complexity, there's no doubt as to why. But as HostMonitor shows, using TSA is not only possible, but also probably easier than you might have thought. By using this script as a template, you can add a TSA icon to any script.
About the Author
You May Also Like