Creating Graphical Wrappers with Tcl/Tk
Michael Hall
Putting a graphical wrapper around standard utility commands provides ease-of-use, convenience, and a logical grouping of functions. This article will show the benefits of putting some standard system commands inside a graphical wrapper using Tcl/Tk.
Strategy There are three classes of commands that work well with a graphical wrapper. I will explain each class and discuss how a wrapper enhances them.
The simplest commands are the "status" commands. They show what's going on with the system or display information about a certain type of entity, such as process, memory, or filesystem. Some examples of these commands are ps, netstat, ipcs, and tbstat (an Informix database status command).
A graphical wrapper for these commands would consist of a set of buttons to invoke the command (with different options) and a scrolling text box to collect and display the output of the command. For example, on an interface for the ps command, you could push a "Scan" button, get a process listing, and scroll the listing back and forth.
Another group of commands that work well with a graphical wrapper could be called "complementary pairs." These are pairs of closely related commands that can show the status of an entity (process, filesystem, network connection, etc.) and can operate on that entity. Instead of status-output only, these commands have status + action. Examples of these complementary pairs are ps + kill, ipcs + ipcrm, lpstat + lpadmin.
A graphical wrapper for these paired commands would have three parts: a set of buttons to invoke the status command with various options, a scrolling text box to show the output of the status command, and additional buttons to invoke the "action" command on selected entries from the text box. For example, using ps and kill, you could click on a "Scan" button, scroll the list to find the process, select the line, and click the "Kill" button.
The third, and final, class of commands that can benefit from a graphical wrapper consists of complementary pairs plus a configuration file. These commands rely on a master file to define the entities that they act upon. Examples of this group would be /etc/fstab + mount + umount + df + fsck + mkfs + ... or /etc/inittab + init + who.
The graphical wrapper for these commands would display the master file in a text box, and buttons would act on or display information about selected entries defined by the master file. For example, the wrapper would show a formatted version of /etc/vfstab. You could select a filesystem, press a button to see which processes are using it (via fuser), press a button to unmount it (umount), press a button to check it (fsck), and finally remount it (mount). Status output of these commands would appear in the scrolling text box.
Example Listing 1 shows the source listing for KP, a Tcl/Tk script to list and kill processes. (See also Figure 1.)
If Tcl/Tk is new to you, a few key concepts may help clarify the code. First, every Tcl statement starts with a command word. Second, a fundamental Tcl data type is a list, and lists are used everywhere in Tcl. A list of statements becomes the body of a procedure. A simple if command has two list arguments - one for the if expression, and one for the if-true clause. Third, as in most shell scripts, a variable must be evaluated to use its value. However, there are a few commands that require the variable name rather than its contents.
Listing 1 starts with a #! heading, which identifies the executable program that should interpret this script. In this case, it is the wish command (although your path may be different if Tcl/Tk was installed in a different place). Comments in Tcl are formatted as comments in shell scripts - they start with a number sign (hash mark, pound sign, etc.).
The first Tcl command appends our custom library path to the list of standard Tcl/Tk library search paths. Tcl has the ability to load source code modules when a routine is referenced at run time. The variable auto_path contains a Tcl list of places where external modules are located. The simplicity of KP relies on many external routines, so Tcl needs to know where to find them. Extending auto_path with the library directory is the way to do that.
Continuing, some global variables need to be set. The first two, ps_user and ps_all are used as arguments to the ps command. ps_user will be used to get a list of processes for a given user. ps_all is used for a listing of all processes on the system. The values shown in the script are fine for System V-based systems. For systems with a BSD heritage, use the other settings shown in the comments.
The next two assignments set initial values for the variables that hold the selected user name and the selected signal name. Later on in the script, menu items will be connected to the variables, so that the contents of the variables will track the selected menu items.
Next, there's a section of two routines that handle the real work in this application. The first routine is called kill_proc, which uses kill to signal a set of processes. The second routine is called scan_proc, which uses ps to get and display a list of process lines. These two routines interface to the complementary commands kill and ps to control and gather information about processes.
As briefly mentioned above, the job of kill_proc is to get a list of selected process lines and send a signal to those processes. This routine first asks the process list panel for a list of index numbers of the currently selected lines. It saves this list in the sellist variable. If this list is empty, no lines were selected, so there is no work to do, and kill_proc returns.
Next, a foreach loop converts the selected process lines to a list of process ID numbers. The first step is to get the text line that is displayed in the process list panel and extract the first two numbers on the line (System V specific). If the process ID value looks bad, the script just skips to the next entry. Other tests can be added here, as shown in the comments, to detect and skip over critical processes that mustn't be disturbed. Once the process listing line has passed the sanity tests, the extracted process ID is appended to a list of process IDs called pid_list.
With this list of selected process IDs, the script constructs a kill command with the following format:
exec kill {signal-name} {pids}
in which the signal name comes from the signal global variable (in turn set from a menu-item). The command is passed to catch for evaluation, and the exit code of the executed command comes back as the return value of catch. If there was an error, it pops up a message and returns. Otherwise, it calls scan_proc to refresh the list of displayed processes.
The job of scan_proc, the second workhorse routine, is to run a ps command, collect the output, and put the output into a scrolling list for the user to view. The global statement tells Tcl that it will be referencing the global variables shown on the line. Without this statement, Tcl would assume that user was a local variable (kill_proc uses a global statement also). First, the program detects whether the variable user has the special value of "Any." If so, then a command line is constructed to list all processes on the system. The command line starts with a vertical bar - the pipe symbol. When this string is opened as a file, the vertical bar tells Tcl to really open a pipe to the command instead. This way, standard file I/O operations can be used with processes as well as files.
If the user variable isn't set to "Any," then a different command line is used to get a list of processes for only the selected user. Given the constructed command line, (whether for all users or one user), a custom library routine read_list is used to open the command (or file), read lines from it, and return the lines as a Tcl list. That list is passed to the fill_list routine, which inserts the contents of a Tcl list into a Tk scrolling list component. The final argument of "0" tells fill_list that the entire contents of each list element (each line from ps) should be placed in the scrolling list component. Other positive values would select individual items from the lists.
These two routines, kill_proc and scan_proc comprise the interface to the complementary pair kill and ps. One sends a signal to selected processes, and one generates a list of processes based on some qualifiers. Both rely on custom routines to make their job easier.
The next section of the script contains routines to create the user interface. The major routine is called make_control_panel. It also references some global variables - specifically, user and signal will be connected to the appropriate menu items.
First, some attributes are set that the window manager will use to identify this process on the screen: the window title, the icon name, and the command line. The next step is to create a complete menu with the custom menu_bar routine. The first argument is the created component name; the next argument is the starting index number (for making menus one by one). The remaining arguments make up a long series of lists, each on a separate line. Each list argument describes a menu item or formatting option for the menu_bar command. The entire list creates three menus: File, Signals, and User.
The first list argument, {menu File}, tells menu_bar to create a new menu called File. The next one, {cmd Quit exit}, creates a command button menu item labeled Quit (under the current File menu), which executes the exit command when selected. The next argument, {end left}, terminates this File menu, and positions it on the left side of the menu bar, after any other previously placed menus.
The remaining lists presented to menu_bar are in two groups: one defines the menu for selecting a signal and the other defines the menu for selecting a user. Each list is built like the File menu, but instead creating of a command button, each generates a series of radio buttons connected to the proper global variable. For example, the list to define the first radio button in the Signal menu is:
{radio " 1 Hangup" signal -HUP}
This tells menu_bar to make a radio button, to label it " 1 Hangup" (the label is quoted to protect the embedded spaces), to connect it to the variable named signal, and to set the contents of the variable to -HUP when this item is selected. It is fairly easy to add additional signals or to make subsets of signals under different menus (termination, IPC, stop/wakeup, etc.). One final detail: the {sep} list in the User menu generates a menu separator bar.
After the menus are made, another custom routine is called to create the scrolling list component. The routine is called mk_list; its first argument is the component name, and the second argument is the name of a routine called when an entry is double-clicked (or when the Return key is pressed when an entry is selected). This routine creates the list box, creates a scroll bar, creates connections between the two, creates a container frame to hold them, and creates key bindings to let the user manipulate the entries from the keyboard.
Next, the row of buttons at the base of the window is created with the buttons routine. Like menu_bar, this routine accepts a series of lists to define the buttons it creates for us. The first argument is the name of the container component that holds all the buttons. The next argument is an optional heading for the entire button group - an empty string means you don't want a heading. The next argument specifies how the buttons are to be placed in the container: here I chose to pack them left to right, to make a row of buttons. The last argument before the button description lists flags whether or not there will be multiple groups of buttons, for example several vertical columns or several horizontal rows. Only one group is needed for this application, so the argument is set to zero.
The remaining argument lists to the buttons command describe the buttons to be created. The first argument list sets the default button width large enough for a label of 4 characters. This makes the buttons change size together as the panel gets resized wider or narrower. Without it, the buttons would have widths proportional to the lengths of their labels, which can look messy sometimes. The next three lists specify individual buttons. Each is a command button, with a label on the button face, and a routine to call when the button is pressed. The Scan button is connected to the scan_proc procedure, and the Kill button is connected to the kill_proc procedure. The Exit button simply calls Tcl's exit command.
Finally, the Tk pack command is used to position the containers that hold all of the visual components. The menu bar goes across the top of the window, the process list is packed underneath that, and the command buttons are positioned against the bottom of the window. Note that both the menu bar and command buttons are allowed to fill out the width of the window, with the -fill x option to pack. The process list is told to use all remaining space in the window, with the -fill both and -expand yes options.
That's all there is to the support routines. The main body of the script comes next. make_control_panel is called to create the interface, and scan_proc is called to fill in the process list so the user has something to look at when KP opens up. Next, the main line code enters a loop. It waits for the user variable to change (via the live user selecting an item under the User menu) and calls scan_proc to update the process list. All the other controls are still active while the loop runs: the process list can be scrolled; menu items can be selected; and the command buttons can request an updated process list or signal a process.
Another example, called NS, is shown in Listing 2 with a screen shot in Figure 2. Its purpose is to capture and display the output of the netstat command. It is very similar to KP, in that it has a few menus, a scrolling information panel, and a row of control buttons. In some ways, it is actually simpler than KP, in that there's less to do - there is less processing of the text, and there's nothing to select in the scrolling list. On the other hand, NS is also a little more complicated than KP because there are more buttons that call netstat with different arguments. (For BSD and Linux systems, the arguments will need some tweaking.)
Tcl's built-in lists, Tk's well-constructed components, and a few routines to hide most of the details, make it fairly easy to write applications like this.
Conclusion There's no reason that day-to-day administrative duties can't look better than a VT100 emulation, and that they can't be made more efficient, more convenient, and more robust. The techniques covered in this article may help you add some buttons to the commands you need the most and enjoy the benefits that a graphical wrapper can bring to the standard system commands.
Availability The sources and library routines for KP (kill processes) and NS (netstat in a window) are available at http://www.balr.com.
About the Author
Mike Hall is a senior consultant with BALR Corporation, and can be reached at mghall@balr.com
|