Chapter 4. User Activity

In the previous chapter, we explored the parts of a user’s identity and how to manage and store it. Now let’s talk about how to manage users while they are active on our systems and networks.

Typical user activities fall into four domains:

Processes

Users run processes that can be spawned, killed, paused, and resumed on the machines we manage. These processes compete for a computer’s finite processing power, adding resource issues to the list of problems a system administrator needs to mediate.

File operations

Most of the time, operations like writing, reading, creating, deleting, and so on take place when a specific user process interacts with files and directories in a filesystem. But under Unix, there’s more to this picture. Unix uses the filesystem as a gateway to more than just file storage. Device control, input/output, and even some process control and network access operations are file operations. We dealt with filesystem administration in Chapter 2, but in this chapter we’ll approach this topic from a user administration perspective.

Network usage

Users can send and receive data over network interfaces on our machines. There is material elsewhere in this book on networking, but we’ll address this issue here from a different perspective.

OS-specific activities

This last domain is a catchall for the OS-specific features that users can access via different APIs. Included in this list are things like GUI element controls, shared memory usage, file-sharing APIs, sound, and so on. This category is so diverse that it would be impossible to do it justice in this book. I recommend that you track down the OS-specific web sites for information on these topics.

Process Management

We’ll begin by looking at ways to deal with the first three of these domains using Perl. Because we’re interested in user administration, the focus here will be on dealing with processes that other users have started.

Windows-Based Operating System Process Control

We’re going to briefly look at four different ways to deal with process control on Windows, because each of these approaches opens up a door to interesting functionality outside the scope of our discussion that is likely to be helpful to you at some point. We’re primarily going to concentrate on two tasks: finding all of the running processes and killing select processes.

Using external binaries

There are a number of programs available to us that display and manipulate processes. The first edition of this book used the programs pulist.exe and kill.exe from the Windows 2000 Resource Kit. Both are still available for download from Microsoft as of this writing and seem to work fine on later versions of the operating system. Another excellent set of process manipulation tools comes from the Sysinternals utility collection, which Mark Russinovich and Bryce Cogswell formerly provided on their Sysinternals web site and which is now available through Microsoft (see the references section at the end of this chapter). This collection includes a suite of utilities called PsTools that can do things the standard Microsoft-supplied tools can’t handle.

For our first example, we’re going to use two programs Microsoft ships with the operating system. The programs tasklist.exe and taskkill.exe work fine for many tasks and are a good choice for scripting in cases where you won’t want to or can’t download other programs to a machine.

By default tasklist produces output in a very wide table that can sometimes be difficult to read. Adding /FO list provides output like this:

Image Name:   System Idle Process
PID:          0
Session Name: Console
Session#:     0
Mem Usage:    16 K
Status:       Running
User Name:    NT AUTHORITY\SYSTEM
CPU Time:     1:09:06
Window Title: N/A

Image Name:   System
PID:          4
Session Name: Console
Session#:     0
Mem Usage:    212 K
Status:       Running
User Name:    NT AUTHORITY\SYSTEM
CPU Time:     0:00:44
Window Title: N/A

Image Name:   smss.exe
PID:          432
Session Name: Console
Session#:     0
Mem Usage:    372 K
Status:       Running
User Name:    NT AUTHORITY\SYSTEM
CPU Time:     0:00:00
Window Title: N/A

Image Name:   csrss.exe
PID:          488
Session Name: Console
Session#:     0
Mem Usage:    3,984 K
Status:       Running
User Name:    NT AUTHORITY\SYSTEM
CPU Time:     0:00:08
Window Title: N/A

Image Name:   winlogon.exe
PID:          512
Session Name: Console
Session#:     0
Mem Usage:    2,120 K
Status:       Running
User Name:    NT AUTHORITY\SYSTEM
CPU Time:     0:00:08
Window Title: N/A

Another format option for tasklist makes using it from Perl pretty trivial: CSV (Comma/Character Separated Values). We’ll talk more about dealing with CSV files in Chapter 5, but here’s a small example that demonstrates how to parse that data:

use Text::CSV_XS;

my $tasklist = "$ENV{'SystemRoot'}\\SYSTEM32\\TASKLIST.EXE";
my $csv = Text::CSV_XS->new();

# /v = verbose (includes User Name), /FO CSV = CSV format, /NH - no header
open my $TASKPIPE, '-|', "$tasklist /v /FO CSV /NH"
    or die "Can't run $tasklist: $!\n";

my @columns;
while (<$TASKPIPE>) {
    next if /^$/;    # skip blank lines in the input
    $csv->parse($_) or die "Could not parse this line: $_\n";
    @columns = ( $csv->fields() )[ 0, 1, 6 ];   # grab name, PID, and User Name
    print join( ':', @columns ), "\n";
}

close $TASKPIPE;

tasklist can also provide some other interesting information, such as the dynamic link libraries (DLLs) used by a particular process. Be sure to run it with the /? switch to see its usage information.

The other program I mentioned, taskkill.exe, is equally easy to use. It takes as an argument a task name (called the “image name”), a process ID, or a more complex filter to determine which processes to kill. I recommend the process ID format to stay on the safe side, since it is very easy to kill the wrong process if you use task names.

taskkill offers two different ways to shoot down processes. The first is the polite death: taskkill.exe /PID <process id> will ask the specified process to shut itself down. However, if we add /F to the command line, it forces the issue: taskkill.exe /F /PID <process id> works more like the native Perl kill() function and kills the process with extreme prejudice.

Using the Win32::Process::Info module

The second approach[19] uses the Win32::Process::Info module, by Thomas R. Wyant. Win32::Process::Info is very easy to use. First, create a process info object, like so:

use Win32::Process::Info;
use strict;

# the user running this script must be able to use DEBUG level privs
my $pi = Win32::Process::Info->new( { assert_debug_priv => 1 } );

The new() method can optionally take a reference to a hash containing configuration information. In this case we set the config variable assert_debug_priv to true because we want our program to use debug-level privileges when requesting information. This is necessary if getting a list of all of the process owners is important to you. If you leave this out, you’ll find that the module (due to the Windows security system) will not be able to fetch the owner of some of the processes. There are some pretty scary warnings in the module’s documentation regarding this setting; I haven’t had any problems with it to date, but you should be sure to read the documentation before you follow my lead.

Next, we retrieve the process information for the machine:

my @processinfo = $pi->GetProcInfo();

@processinfo is now an array of references to anonymous hashes. Each anonymous hash has a number of keys (such as Name, ProcessId, CreationDate, and ExecutablePath), each with its expected value. To display our process info in the same fashion as the example from the last section, we could use the following code:

use Win32::Process::Info;

my $pi = Win32::Process::Info->new( { assert_debug_priv => 1 } );
my @processinfo = $pi->GetProcInfo();

foreach my $process (@processinfo) {
    print join( ':',
        $process->{'Name'}, $process->{'ProcessId'},
        $process->{'Owner'} ),
        "\n";
}

Once again, we get output like this:

System Idle Process:0:
System:4:
smss.exe:432:NT AUTHORITY\SYSTEM
csrss.exe:488:NT AUTHORITY\SYSTEM
winlogon.exe:512:NT AUTHORITY\SYSTEM
services.exe:556:NT AUTHORITY\SYSTEM
lsass.exe:568:NT AUTHORITY\SYSTEM
svchost.exe:736:NT AUTHORITY\SYSTEM
svchost.exe:816:NT AUTHORITY\NETWORK SERVICE
svchost.exe:884:NT AUTHORITY\SYSTEM
svchost.exe:960:NT AUTHORITY\SYSTEM
svchost.exe:1044:NT AUTHORITY\NETWORK SERVICE
svchost.exe:1104:NT AUTHORITY\LOCAL SERVICE
ccSetMgr.exe:1172:NT AUTHORITY\SYSTEM
ccEvtMgr.exe:1200:NT AUTHORITY\SYSTEM
spoolsv.exe:1324:NT AUTHORITY\SYSTEM
...

Win32::Process::Info provides more info about a process than just these fields (perhaps more than you will ever need). It also has one more helpful feature: it can show you the process tree for all processes or just a particular process. This allows you to display the subprocesses for each process (i.e., the list of processes that process spawned) and the subprocesses for those subprocesses, and so on.

So, for example, if we wanted to see all of the processes spawned by one of the processes just listed, we could write the following:

use Win32::Process::Info;
use Data::Dumper;

my $pi = Win32::Process::Info->new( { assert_debug_priv => 1 } );

# PID 884 picked for this example because it has a small number of children
my %sp = $pi->Subprocesses(884);

print Dumper (\%sp);

This yields:

$VAR1 = {
          '3320' => [],
          '884' => [
                     3320
                   ]
        };

which shows that this instance of svchost.exe (PID 884) has one child, the process with PID 3320. That process does not have any children.

Using the GUI control modules (Win32::Setupsup and Win32::GuiTest)

Of the approaches we’ll consider, this third approach is probably the most fun. In this section we’ll look at a module by Jens Helberg called Win32::Setupsup and a module by Ernesto Guisado, Jarek Jurasz, and Dennis K. Paulsen called Win32::GuiTest. They have similar functionality but achieve the same goals a little differently. We’ll look primarily at Win32::Setupsup, with a few choice examples from Win32::GuiTest.

Note

In the interest of full disclosure, it should be mentioned that (as of this writing) Win32::Setupsup had not been developed since October 2000 and is kind of hard to find (see the references at the end of this chapter). It still works well, though, and it has features that aren’t found in Win32::GuiTest; hence its inclusion here. If its orphan status bothers you, I recommend looking at Win32::GuiTest first to see if it meets your needs.

Win32::Setupsup is called “Setupsup” because it is primarily designed to supplement software installation (which often uses a program called setup.exe).

Some installers can be run in so-called “silent mode” for totally automated installation. In this mode they ask no questions and require no “OK” buttons to be pushed, freeing the administrator from having to babysit the install. Software installation mechanisms that do not offer this mode (and there are far too many of them) make a system administrator’s life difficult. Win32::Setupsup helps deal with these deficiencies: it can find information on running processes and manipulate them (or manipulate them dead if you so choose).

Note

For instructions on getting and installing Win32::Setupsup, refer to the section Module Information for This Chapter.

With Win32::Setupsup, getting the list of running processes is easy. Here’s an example:

use Win32::Setupsup;
use Perl6::Form;

my $machine = '';    # query the list on the current machine

# define the output format for Perl6::Form
my $format = '{<<<<<<<}        {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}';

my ( @processlist, @threadlist );
Win32::Setupsup::GetProcessList( $machine, \@processlist, \@threadlist )
    or die 'process list error: ' . Win32::Setupsup::GetLastError() . "\n";

pop(@processlist);    # remove the bogus entry always appended to the list

print <<'EOH';
Process ID       Process Name
==========       ===============================
EOH

foreach my $processlist (@processlist) {
    print form $format, $processlist->{pid}, $processlist->{name};
}

Killing processes is equally easy:

KillProcess($pid, $exitvalue, $systemprocessflag) or
  die 'Unable to kill process: ' . Win32::Setupsup::GetLastError(  ) . "\n";

The last two arguments are optional. The second argument kills the process and sets its exit value accordingly (by default, it is set to 0). The third argument allows you to kill system-run processes (providing you have the Debug Programs user right).

That’s the boring stuff. We can take process manipulation to yet another level by interacting with the windows a running process may have open. To list all of the windows available on the desktop, we use:

Win32::Setupsup::EnumWindows(\@windowlist) or
    die 'process list error: ' . Win32::Setupsup::GetLastError(  ) . "\n";

@windowlist now contains a list of window handles that are converted to look like normal numbers when you print them. To learn more about each window, you can use a few different functions. For instance, to find the titles of each window, you can use GetWindowText() like so:

use Win32::Setupsup;

my @windowlist;
Win32::Setupsup::EnumWindows( \@windowlist )
    or die 'process list error: ' . Win32::Setupsup::GetLastError() . "\n";

my $text;
foreach my $whandle (@windowlist) {
    if ( Win32::Setupsup::GetWindowText( $whandle, \$text ) ) {
        print "$whandle: $text", "\n";
    }
    else {
        warn "Can't get text for $whandle"
            . Win32::Setupsup::GetLastError() . "\n";
    }
}

Here’s a little bit of sample output:

66130: chapter04 - Microsoft Word
66184: Style
194905150:
66634: setupsup - WordPad
65716: Fuel
328754: DDE Server Window
66652:
66646:
66632: OleMainThreadWndName

As you can see, some windows have titles, while others do not. Observant readers might notice something else interesting about this output. Window 66130 belongs to a Microsoft Word session that is currently running (it is actually the one in which this chapter was composed). Window 66184 looks vaguely like the name of another window that might be connected to Microsoft Word. How can we tell if they are related?

Win32::Setupsup has an EnumChildWindows() function that can show us the children of any given window. Let’s use it to write something that will show us a basic tree of the current window hierarchy:

use Win32::Setupsup;

my @windowlist;
# get the list of windows
Win32::Setupsup::EnumWindows( \@windowlist )
    or die 'process list error: ' . Win32::Setupsup::GetLastError() . "\n";

# turn window handle list into a hash
# NOTE: this conversion populates the hash with plain numbers and
# not actual window handles as keys. Some functions, like
# GetWindowProperties (which we'll see in a moment), can't use these
# converted numbers. Caveat implementor.
my %windowlist;
for (@windowlist) { $windowlist{$_}++; }

# check each window for children
my %children;
foreach my $whandle (@windowlist) {
    my @children;
    if ( Win32::Setupsup::EnumChildWindows( $whandle, \@children ) ) {

        # keep a sorted list of children for each window
        $children{$whandle} = [ sort { $a <=> $b } @children ];

        # remove all children from the hash; we won't directly
        # iterate over them
        foreach my $child (@children) {
            delete $windowlist{$child};
        }
    }
}

# iterate through the list of windows and recursively print
# each window handle and its children (if any)
foreach my $window ( sort { $a <=> $b } keys %windowlist ) {
    PrintFamily( $window, 0, %children );
}

# print a given window handle number and its children (recursively)
sub PrintFamily {

    # starting window - how deep in a tree are we?
    my ( $startwindow, $level, %children ) = @_;

    # print the window handle number at the appropriate indentation
    print( ( '  ' x $level ) . "$startwindow\n" );

    return unless ( exists $children{$startwindow} );    # no children, done

    # otherwise, we have to recurse for each child
    $level++;
    foreach my $childwindow ( @{ $children{$startwindow} } ) {
        PrintFamily( $childwindow, $level, %children );
    }
}

There’s one last window property function we should look at before moving on: GetWindowProperties(). GetWindowProperties() is basically a catchall for the rest of the window properties we haven’t seen yet. For instance, using GetWindowProperties() we can query the process ID for the process that created a specific window. This could be combined with some of the functionality we just saw for the Win32::Process::Info module.

The Win32::Setupsup documentation contains a list of the available properties that can be queried. Let’s use one of them to write a very simple program that will print the coordinates of a rectangular window on the desktop. GetWindowProperties() takes three arguments: a window handle, a reference to an array that contains the names of the properties to query, and a reference to a hash where the query results will be stored. Here’s the code we need for our task:

use Win32::Setupsup;

# Convert window ID into a form that GetWindowProperties can cope with.
# Note: 'U' is a pack template that is only available in Perl 5.6+ releases.

my $whandle = unpack 'U', pack 'U', $ARGV[0];
my %info;
Win32::Setupsup::GetWindowProperties( $whandle, ['rect'], \%info );

print "\t" . $info{rect}{top} . "\n";
print $info{rect}{left} . ' -' . $whandle . '- ' . $info{rect}{right} . "\n";
print "\t" . $info{rect}{bottom} . "\n";

The output is a bit cutesy. Here’s a sample showing the top, left, right, and bottom coordinates of the window with handle 66180:

     154
272 −66180- 903
     595

GetWindowProperties() returns a special data structure for only one property, rect. All of the others will simply show up in the referenced hash as normal keys and values. If you are uncertain about the properties being returned by Perl for a specific window, the windowse utility is often helpful.

Now that we’ve seen how to determine various window properties, wouldn’t it be spiffy if we could make changes to some of these properties? For instance, it might be useful to change the title of a particular window. With this capability, we could create scripts that used the window title as a status indicator:

"Prestidigitation In Progress ... 32% complete"

Making this change to a window is as easy as a single function call:

Win32::Setupsup::SetWindowText($handle,$text);

We can also set the rect property we just saw. This code makes the specified window jump to the position we’ve specified:

use Win32::Setupsup;

my %info;
$info{rect}{left}   = 0;
$info{rect}{right}  = 600;
$info{rect}{top}    = 10;
$info{rect}{bottom} = 500;
my $whandle = unpack 'U', pack 'U', $ARGV[0];
Win32::Setupsup::SetWindowProperties( $whandle, \%info );

I’ve saved the most impressive function for last. With SendKeys(), it is possible to send arbitrary keystrokes to any window on the desktop. For example:

use Win32::Setupsup;

my $texttosend = "\\DN\\Low in the gums";
my $whandle = unpack 'U', pack 'U', $ARGV[0];
Win32::Setupsup::SendKeys( $whandle, $texttosend, 0 ,0 );

This will send a “down cursor key” followed by some text to the specified window. The arguments to SendKeys() are pretty simple: window handle, text to send, a flag to determine whether a window should be activated for each keystroke, and an optional time between keystrokes. Special key codes like the down cursor are surrounded by backslashes. The list of available keycodes can be found in the module’s documentation.

Before we move on to another tremendously useful way to work with user processes in the Windows universe, I want to briefly look at a module that shares some functionality with Win32::Setupsup but can do even more interesting stuff. Like Win32::Setupsup, Win32::GuiTest can return information about active windows and send keystrokes to applications. However, it offers even more powerful functionality.

Here’s an example slightly modified from the documentation (stripped of comments and error checking, be sure to see the original) that demonstrates some of this power:

use Win32::GuiTest qw(:ALL);

system("start notepad.exe");
sleep 1;

MenuSelect("F&ormat|&Font");
sleep(1);

my $fontdlg = GetForegroundWindow();

my ($combo) = FindWindowLike( $fontdlg, '', 'ComboBox', 0x470 );

for ( GetComboContents($combo) ) {
    print "'$_'" . "\n";
}

SendKeys("{ESC}%{F4}");

This code starts up notepad, asks it to open its font settings by choosing the appropriate menu item, and then reads the contents of the resulting dialog box and prints what it finds. It then sends the necessary keystrokes to dismiss the dialog box and tell notepad to quit. The end result is a list of monospaced fonts available on the system that looks something like this:

'Arial'
'Arial Black'
'Comic Sans MS'
'Courier'
'Courier New'
'Estrangelo Edessa'
'Fixedsys'
'Franklin Gothic Me
'Gautami'
'Georgia'
'Impact'
'Latha'
'Lucida Console'
'Lucida Sans Unicod
'Mangal'
'Marlett'
'Microsoft Sans Ser
'Modern'
'MS Sans Serif'

Let’s look at one more example (again, adapted from the module’s documentation because it offers great example code):

use Win32::GuiTest qw(:ALL);

system 'start notepad';
sleep 1;

my $menu = GetMenu( GetForegroundWindow() );
menu_parse($menu);

SendKeys("{ESC}%{F4}");

sub menu_parse {
    my ( $menu, $depth ) = @_;
    $depth ||= 0;

    foreach my $i ( 0 .. GetMenuItemCount($menu) - 1 ) {
        my %h = GetMenuItemInfo( $menu, $i );
        print '   ' x $depth;
        print "$i  ";
        print $h{text} if $h{type} and $h{type} eq 'string';
        print "------" if $h{type} and $h{type} eq 'separator';
        print "UNKNOWN" if not $h{type};
        print "\n";

        my $submenu = GetSubMenu( $menu, $i );
        if ($submenu) {
            menu_parse( $submenu, $depth + 1 );
        }
    }
}

As in the previous example, we begin by spinning up notepad. We can then examine the menus of the application in the foreground window, determining the number of top-level menu items and then iterating over each item (printing the information and looking for submenus of each item as we go). If we find a submenu, we recursively call menu_parse() to examine it. Once we’ve completed the menu walk, we send the keys to close the notepad window and quit the application.

The output looks like this:

0  &File
   0  &New      Ctrl+N
   1  &Open...  Ctrl+O
   2  &Save     Ctrl+S
   3  Save &As...
   4  ------
   5  Page Set&up...
   6  &Print... Ctrl+P
   7  ------
   8  E&xit
1  &Edit
   0  &Undo     Ctrl+Z
   1  ------
   2  Cu&t      Ctrl+X
   3  &Copy     Ctrl+C
   4  &Paste    Ctrl+V
   5  De&lete   Del
   6  ------
   7  &Find...  Ctrl+F
   8  Find &Next        F3
   9  &Replace...       Ctrl+H
   10  &Go To...        Ctrl+G
   11  ------
   12  Select &All      Ctrl+A
   13  Time/&Date       F5
2  F&ormat
   0  &Word Wrap
   1  &Font...
3  &View
   0  &Status Bar
4  &Help
   0  &Help Topics
   1  ------
   2  &About Notepad

Triggering known menu items from a script is pretty cool, but it’s even cooler to have the power to determine which menu items are available. This lets us write much more adaptable scripts.

We’ve only touched on a few of Win32::GuiTest’s advanced features here. Some of the other impressive features include the ability to read the text context of a window using WMGetText() and the ability to select individual tabs in a window with SelectTabItem(). See the documentation and the example directory (eg) for more details.

With the help of these two modules, we’ve taken process control to an entirely new level. Now it is possible to remotely control applications (and parts of the OS) without the explicit cooperation of those applications. We don’t need them to offer command-line support or a special API; we have the ability to essentially script a GUI, which is useful in a myriad of system administration contexts.

Using Windows Management Instrumentation (WMI)

Let’s look at one final approach to Windows process control before we switch to another operating system. By now you’ve probably figured out that each of these approaches is not only good for process control, but also can be applied in many different ways to make Windows system administration easier. If you had to pick the approach that would yield the most reward in the long term to learn, WMI-based scripting is probably it. The first edition of this book called Windows Management Instrumentation “Futureland” because it was still new to the scene when the book was being written. In the intervening time, Microsoft, to its credit, has embraced the WMI framework as its primary interface for administration of not just its operating systems, but also its other products, such as MS SQL Server and Microsoft Exchange.

Unfortunately, WMI is one of those not-for-the-faint-of-heart technologies that gets very complex very quickly. It is based on an object-oriented model that has the power to represent not only data, but also relationships between objects. For instance, it is possible to create an association between a web server and the storage device that holds the data for that server, so that if the storage device fails, a problem for the web server will be reported as well. We don’t have the space to deal with this complexity here, so we’re just going to skim the surface of WMI by providing a small and simple introduction, followed by a few code samples.

If you want to get a deeper look at this technology, I recommend searching for WMI-related content at http://msdn.microsoft.com. You should also have a look at the information found at the Distributed Management Task Force’s website. In the meantime, here is a brief synopsis to get you started.

WMI is the Microsoft implementation and extension of an unfortunately named initiative called the Web-Based Enterprise Management initiative, or WBEM for short. Though the name conjures up visions of something that requires a browser, it has virtually nothing to do with the World Wide Web. The companies that were part of the Distributed Management Task Force (DMTF) wanted to create something that could make it easier to perform management tasks using browsers. Putting the name aside, it is clearer to say that WBEM defines a data model for management and instrumentation information. It provides specifications for organizing, accessing, and moving this data around. WBEM is also meant to offer a cohesive frontend for accessing data provided by other management protocols, such as the Simple Network Management Protocol (SNMP), discussed in Chapter 12, and the Common Management Information Protocol (CMIP).

Data in the WBEM world is organized using the Common Information Model (CIM). CIM is the source of the power and complexity in WBEM/WMI. It provides an extensible data model that contains objects and object classes for any physical or logical entity one might want to manage. For instance, there are object classes for entire networks, and objects for single slots in specific machines. There are objects for hardware settings and objects for software application settings. On top of this, CIM allows us to define object classes that describe relationships between other objects.

This data model is documented in two parts: the CIM Specification and the CIM Schema. The former describes the how of CIM (how the data will be specified, its connection to prior management standards, etc.), while the latter provides the what of CIM (the actual objects). This division may remind you of the SNMP SMI and MIB relationship (see Appendix G and Chapter 12).

In practice, you’ll be consulting the CIM Schema more than the CIM Specification once you get the hang of how the data is represented. The schema format (called MOF, for Managed Object Format) is fairly easy to read.

The CIM Schema has two layers:

  • The core model for objects and classes useful in all types of WBEM interaction.

  • The common model for generic objects that are vendor- and operating system-independent. Within the common model there are currently 15 specific areas, including Systems, Devices, Applications, Networks, and Physical.

Built on top of these two layers can be any number of extension schemas that define objects and classes for vendor- and OS-specific information. WMI is one WBEM implementation that makes heavy use of this extension mechanism.

A crucial part of WMI that distinguishes it from generic WBEM implementations is the Win32 Schema, an extension schema for Win32-specific information built on the core and common models. WMI also adds to the generic WBEM framework by providing Win32-specific access mechanisms to the CIM data.[20] Using this schema extension and set of data access methods, we can explore how to perform process control operations using WMI in Perl.

WMI offers two different approaches for getting at management data: object-oriented and query-based. With the former you specify the specific object or container of objects that contains the information you seek, while with the latter you construct a SQL-like[21] query that returns a result set of objects containing your desired data. We’ll give a simple example of each approach so you can see how they work.

The Perl code that follows does not appear to be particularly complex, so you may wonder about the earlier “gets very complex very quickly” description. The code looks simple because:

  • We’re only scratching the surface of WMI. We’re not even going to touch on subjects like associations (i.e., relationships between objects and object classes).

  • The management operations we are performing are simple. Process control in this context will consist of querying the running processes and being able to terminate them at will. These operations are easy in WMI using the Win32 Schema extension.

  • Our samples hide the complexity of translating WMI documentation and code samples in VBScript/JScript to Perl code. See Appendix F for some help with that task.

  • Our samples hide the opaqueness of the debugging process. When WMI-related Perl code fails (especially code of the object-oriented flavor), it provides very little information that would help you debug the problem. You may receive error messages, but they never say ERROR: YOUR EXACT PROBLEM IS.... You’re more likely to get back a message like wbemErrFailed 0x8004100 or just an empty data structure. To be fair to Perl, most of this opaqueness comes from Perl’s role in this process: it is acting as a frontend to a set of fairly complex multilayered operations that don’t concern themselves with passing back useful feedback when something fails.

I know this sounds pretty grim, so let me offer some potentially helpful advice before we actually get into the code itself:

  • Look at all of the Win32::OLE sample code you can lay your hands on. The ActiveState Win32-Users mailing list archive found at http://aspn.activestate.com/ASPN/Mail is a good source for this code. If you compare this sample code to equivalent VBScript examples, you’ll start to understand the necessary translation idioms. Appendix F and the section Active Directory Service Interfaces in Chapter 9 may also help.

  • Make friends with the Perl debugger, and use it to try out code snippets as part of this learning process. There are also several REPL[22]-modules available on CPAN, such as App::REPL, Devel::REPL, and Shell::Perl, that can make interactive prototyping easier. Other integrated development environment (IDE) tools may also offer this functionality.

  • Keep a copy of the WMI SDK handy. The documentation and the VBScript code examples are very helpful.

  • Use the WMI object browser in the WMI SDK frequently. It helps you get the lay of the land.

Now let’s get to the Perl part of this section. Our initial task will be to determine what information we can retrieve about Windows processes and how we can interact with that information.

First we need to establish a connection to a WMI namespace. A namespace is defined in the WMI SDK as “a unit for grouping classes and instances to control their scope and visibility.” In this case, we’re interested in connecting to the root of the standard cimv2 namespace, which contains all of the data that is interesting to us.

We will also have to set up a connection with the appropriate security privileges and impersonation level. Our program will need to be given the privilege to debug a process and to impersonate us; in other words, it has to run as the user calling the script. After we get this connection, we will retrieve a Win32_Process object (as defined in the Win32 Schema).

There is a hard way and an easy way to create this connection and get the object. We’ll look at both in the first example, so you get an idea of what the methods entail. Here’s the hard way, with its explanation to follow:

use Win32::OLE('in');

my $server = ''; # connect to local machine

# get an SWbemLocator object
my $lobj = Win32::OLE->new('WbemScripting.SWbemLocator') or
    die "can't create locator object: ".Win32::OLE->LastError()."\n";

# set the impersonation level to "impersonate"
$lobj->{Security_}->{impersonationlevel} = 3;

# use it to get an SWbemServices object
my $sobj = $lobj->ConnectServer($server, 'root\cimv2') or
    die "can't create server object: ".Win32::OLE->LastError()."\n";

# get the schema object
my $procschm = $sobj->Get('Win32_Process');

The hard way involves:

  • Getting a locator object, used to find a connection to a server object

  • Setting the impersonation level so our program will run with our privileges

  • Using the locator object to get a server connection to the cimv2 WMI namespace

  • Using this server connection to retrieve a Win32_Process object

Doing it this way is useful in cases where you need to operate on the intermediate objects. However, we can do this all in one step using a COM moniker’s display name. According to the WMI SDK, “in Common Object Model (COM), a moniker is the standard mechanism for encapsulating the location and binding of another COM object. The textual representation of a moniker is called a display name.” Here’s an easy way to do the same thing as the previous code snippet:

use Win32::OLE('in');

my $procschm = Win32::OLE->GetObject(
    'winmgmts:{impersonationLevel=impersonate}!Win32_Process')
    or die "can't create server object: ".Win32::OLE->LastError()."\n";

Now that we have a Win32_Process object in hand, we can use it to show us the relevant parts of the schema that represent processes under Windows. This includes all of the available Win32_Process properties and methods we can use. The code to do this is fairly simple; the only magic is the use of the Win32::OLE in operator. To explain this, we need a quick digression.

Our $procschm object has two special properties, Properties_ and Methods_. Each holds a special child object, known as a collection object in COM parlance. A collection object is just a parent container for other objects; in this case, they are holding the schema’s property method description objects. The in operator just returns an array with references to each child object of a container object.[23] Once we have this array, we can iterate through it, returning the Name property of each child object as we go. Here’s what the code looks like:

use Win32::OLE('in');

# connect to namespace, set the impersonation level, and retrieve the
# Win32_process object just by using a display name
my $procschm = Win32::OLE->GetObject(
    'winmgmts:{impersonationLevel=impersonate}!Win32_Process')
    or die "can't create server object: ".Win32::OLE->LastError()."\n";

print "--- Properties ---\n";
print join("\n",map {$_->{Name}}(in $procschm->{Properties_}));
print "\n--- Methods ---\n";
print join("\n",map {$_->{Name}}(in $procschm->{Methods_}));

The output (on a Windows XP SP2 machine) looks like this:

--- Properties ---
Caption
CommandLine
CreationClassName
CreationDate
CSCreationClassName
CSName
Description
ExecutablePath
ExecutionState
Handle
HandleCount
InstallDate
KernelModeTime
MaximumWorkingSetSize
MinimumWorkingSetSize
Name
OSCreationClassName
OSName
OtherOperationCount
OtherTransferCount
PageFaults
PageFileUsage
ParentProcessId
PeakPageFileUsage
PeakVirtualSize
PeakWorkingSetSize
Priority
PrivatePageCount
ProcessId
QuotaNonPagedPoolUsage
QuotaPagedPoolUsage
QuotaPeakNonPagedPoolUsage
QuotaPeakPagedPoolUsage
ReadOperationCount
ReadTransferCount
SessionId
Status
TerminationDate
ThreadCount
UserModeTime
VirtualSize
WindowsVersion
WorkingSetSize
WriteOperationCount
WriteTransferCount
--- Methods ---
Create
Terminate
GetOwner
GetOwnerSid
SetPriority
AttachDebugger

Now let’s get down to the business at hand. To retrieve a list of running processes, we need to ask for all instances of Win32_Process objects:

use Win32::OLE('in');

# perform all of the initial steps in one swell foop

my $sobj = Win32::OLE->GetObject(
    'winmgmts:{impersonationLevel=impersonate}')
    or die "can't create server object: ".Win32::OLE->LastError()."\n";

foreach my $process (in $sobj->InstancesOf("Win32_Process")){
  print $process->{Name}." is pid #".$process->{ProcessId},"\n";
}

Our initial display name did not include a path to a specific object (i.e., we left off !Win32_Process). As a result, we receive a server connection object. When we call the InstancesOf() method, it returns a collection object that holds all of the instances of that particular object. Our code visits each object in turn and prints its Name and ProcessId properties. This yields a list of all the running processes.

If we wanted to be a little less beneficent when iterating over each process, we could instead use one of the methods listed earlier:

foreach $process (in $sobj->InstancesOf("Win32_Process")){
  $process->Terminate(1);
}

This will terminate every process running. I do not recommend that you run this code as is; customize it for your specific needs by making it more selective.

One last note before we move on. Earlier in this section I mentioned that there are two ways to query information using WMI: the object-oriented and query-based approaches. Up to now we’ve been looking at the fairly straightforward object-oriented approach. Here’s a small sample using the query-based approach, just to pique your interest. First, let’s recreate the output from the preceding sample. The highlighted line is the key change here, because it uses WQL instead of InstancesOf() to retrieve all of the process objects:

use Win32::OLE('in');

my $sobj = Win32::OLE->GetObject('winmgmts:{impersonationLevel=impersonate}')
    or die 'can't create server object: ' . Win32::OLE->LastError() . "\n";

my $query = $sobj->ExecQuery('SELECT Name, ProcessId FROM Win32_Process');
foreach my $process ( in $query ) {
  print $process->{Name} . ' is pid #' . $process->{ProcessId}, "\n";
}

Now we can start throwing in SQL-like syntax in the highlighted query string. For example, if we only wanted to see the process IDs of the svchost.exe processes running on the system, we could write:

use Win32::OLE('in');

my $sobj = Win32::OLE->GetObject('winmgmts:{impersonationLevel=impersonate}')
    or die "can't create server object: " . Win32::OLE->LastError() . "\n";

my $query = $sobj->ExecQuery(
    'SELECT ProcessId FROM Win32_Process WHERE Name = "svchost.exe"');
print "SvcHost processes: "
    . join( ' ', map { $_->{ProcessId} } ( in $query) ), "\n";

WQL can handle queries with other SQL-like stanzas. For example, the following is valid WQL to retrieve information on all running processes that have names that begin with “svc”:

SELECT * from Win32_Process WHERE Name LIKE "svc%"

If you are SQL-literate (even if the sum of your knowledge comes from Appendix D in this book), this may be a direction you want to explore.

Now you have the knowledge necessary to begin using WMI for process control. WMI has Win32 extensions for many other parts of the operating system, including the registry and the event log facility.

This is as far as we’re going to delve into process control on Windows. Now let’s turn our attention to another major operating system.

Unix Process Control

Strategies for Unix process control offer another multiple-choice situation. Luckily, these choices aren’t nearly as complex as those that Windows offers. When we speak of process control under Unix, we’re referring to three operations:

  1. Enumerating the list of running processes on a machine

  2. Changing their priorities or process groups

  3. Terminating the processes

For the final two of these operations, there are Perl functions to do the job: setpriority(), setpgrp(), and kill(). The first one offers us a few options. To list running processes, you can:

  • Call an external program like ps.

  • Take a crack at deciphering /dev/kmem.

  • Look through the /proc filesystem (for Unix versions that have one).

  • Use the Proc::ProcessTable module.

Let’s discuss each of these approaches. For the impatient reader, I’ll reveal right now that Proc::ProcessTable is my preferred technique. You may want to just skip directly to the discussion of that module, but I recommend reading about the other techniques anyway, since they may come in handy in the future.

Calling an external program

Common to all modern Unix variants is a program called ps, used to list running processes. However, ps is found in different places in the filesystem on different Unix variants, and the command-line switches it takes are also not consistent across variants. Therein lies one problem with this option: it lacks portability.

An even more annoying problem is the difficulty in parsing the output (which also varies from variant to variant). Here’s a snippet of output from ps on an ancient SunOS machine:

USER       PID %CPU %MEM   SZ  RSS TT STAT START  TIME COMMAND
dnb        385  0.0  0.0  268    0 p4 IW   Jul  2  0:00 /bin/zsh
dnb      24103  0.0  2.610504 1092 p3 S    Aug 10 35:49 emacs
dnb        389  0.0  2.5 3604 1044 p4 S    Jul  2 60:16 emacs
remy     15396  0.0  0.0  252    0 p9 IW   Jul  7  0:01 -zsh (zsh)
sys        393  0.0  0.0   28    0 ?  IW   Jul  2  0:02 in.identd
dnb      29488  0.0  0.0   68    0 p5 IW   20:15   0:00 screen
dnb      29544  0.0  0.4   24  148 p7 R    20:39   0:00 less
dnb       5707  0.0  0.0  260    0 p6 IW   Jul 24  0:00 -zsh (zsh)
root     28766  0.0  0.0  244    0 ?  IW   13:20   0:00 -:0 (xdm)

Notice the third line. Two of the columns have run together, making parsing this output an annoying task. It’s not impossible, just vexing. Some Unix variants are kinder than others in this regard (for example, later operating systems from Sun don’t have this problem), but it is something you may have to take into account.

The Perl code required for this option is straightforward: use open() to run ps, while(<FH>){...} to read the output, and split(), unpack(), or substr() to parse it. You can find a recipe for this in the Perl Cookbook , by Tom Christiansen and Nathan Torkington (O’Reilly).

Examining the kernel process structures

I only mention this option for completeness’s sake. It is possible to write code that opens up a device like /dev/kmem and accesses the current running kernel’s memory structures. With this access, you can track down the current process table in memory and read it. However, given the pain involved (taking apart complex binary structures by hand), and its extreme nonportability (a version difference within the same operating system is likely to break your program), I’d strongly recommend against using this option.[24]

If you decide not to heed this advice, you should begin by memorizing the Perl documentation for pack(), unpack(), and the header files for your kernel. Open the kernel memory file (often /dev/kmem), then read() and unpack() to your heart’s content. You may find it instructive to look at the source for programs like top that perform this task using a great deal of C code. Our next option offers a slightly better version of this method.

Using the /proc filesystem

One of the more interesting additions to Unix found in most of the current variants is the /proc filesystem. This is a magical filesystem that has nothing to do with data storage. Instead, it provides a file-based interface for the running process table of a machine. A “directory” named after the process ID appears in this filesystem for each running process. In this directory are a set of “files” that provide information about that process. One of these files can be written to, thus allowing control of the process.

It’s a really clever concept, and that’s the good news. The bad news is that each Unix vendor/developer team decided to take this clever concept and run with it in a different direction. As a result, the files found in a /proc directory are often variant-specific, both in name and format. For a description of which files are available and what they contain, you will need to consult the manual pages (usually found in sections 4, 5, or 8) for procfs or mount_ procfs on your system.

The one fairly portable use of the /proc filesystem is the enumeration of running processes. If we want to list just the process IDs and their owners, we can use Perl’s directory and lstat() operators:

opendir my $PROC, '/proc' or die "Unable to open /proc:$!\n";

# only stat the items in /proc that look like PIDs
for my $process (grep /^\d+$/, readdir($PROC)){
    print "$process\t". getpwuid((lstat "/proc/$process")[4])."\n";
}

closedir $PROC;

If you are interested in more information about a process, you will have to open and unpack() the appropriate binary file in the /proc directories. Common names for this file are status and psinfo. The manual pages cited a moment ago should provide details about the C structure found in this file, or at least a pointer to a C include file that documents this structure. Because these are operating system-specific (and OS version-specific) formats, you’re still going to run into the problem of program fragility mentioned in the discussion of the previous option.

You may be feeling discouraged at this point because all of our options so far look like they require code with lots of special cases (one for each version of each operating system we wish to support). Luckily, we have one more option up our sleeve that may help in this regard.

Using the Proc::ProcessTable module

Daniel J. Urist (with the help of some volunteers) has been kind enough to write a module called Proc::ProcessTable that offers a consistent interface to the process table for the major Unix variants. It hides the vagaries of the different /proc or kmem implementations for you, allowing you to write relatively portable code.

Simply load the module, create a Proc::ProcessTable::Process object, and run methods from that object:

use Proc::ProcessTable;

my $tobj = new Proc::ProcessTable;

This object uses Perl’s tied variable functionality to present a real-time view of the system. You do not need to call a special function to refresh the object; each time you access it, it re-reads the process table.

To get at this information, you call the object method table():

my $proctable = $tobj->table( );

table() returns a reference to an array with members that are references to individual process objects. Each of these objects has its own set of methods that returns information about that process. For instance, here’s how you would get a listing of the process IDs and owners:

use Proc::ProcessTable;

my $tobj      = new Proc::ProcessTable;

my $proctable = $tobj->table();

foreach my $process (@$proctable) {
  print $process->pid . "\t" . getpwuid( $process->uid ) . "\n";
}

If you want to know which process methods are available on your Unix variant, the fields() method of your Proc::ProcessTable object ($tobj in the preceding code) will return a list for you.

Proc::ProcessTable also adds three other methods to each process object—kill(), priority(), and pgrp()—which are just frontends to the built-in Perl function we mentioned at the beginning of this section.

To bring us back to the big picture, let’s look at some of the uses of these process control techniques. We started to examine process control in the context of user actions, so let’s look at a few teeny scripts that focus on these actions. We will use the Proc::ProcessTable module on Unix for these examples, but these ideas are not operating system-specific.

The first example is slightly modified from the documentation for Proc::ProcessTable:

use Proc::ProcessTable;

my $t = new Proc::ProcessTable;

foreach my $p (@{$t->table}){
  if ($p->pctmem > 95){
    $p->kill(9);
  }
}

When run on the Unix variants that provide the pctmem() method (most do), this code will shoot down any process consuming 95% of the machine’s memory. As it stands, it’s probably too ruthless to be used in real life. It would be much more reasonable to add something like this before the kill() command:

print 'about to nuke '.$p->pid."\t". getpwuid($p->uid)."\n";
print 'proceed? (yes/no) ';
chomp($ans = <>);
next unless ($ans eq 'yes');

There’s a bit of a race condition here: it is possible that the system state will change during the delay induced by prompting the user. Given that we are only prompting for huge processes, though, and huge processes are those least likely to change state in a short amount of time, we’re probably fine coding this way. If you wanted to be pedantic, you would probably collect the list of processes to be killed first, prompt for input, and then recheck the state of the process table before actually killing the desired processes. This doesn’t remove the race condition, but it does make it much less likely to occur.

There are times when death is too good for a process. Sometimes it is important to notice that a process is running while it is running so that some real-life action (like “user attitude correction”) can be taken. For example, at our site we have a policy against the use of Internet Relay Chat (IRC) bots. Bots are daemon processes that connect to an IRC network of chat servers and perform automated actions. Though bots can be used for constructive purposes, these days they play a mostly antisocial role on IRC. We’ve also had security breaches come to our attention because the first (and often only) thing the intruder has done is put up an IRC bot of some sort. As a result, noting their presence on our system without killing them is important to us.

The most common bot by far is called eggdrop. If we wanted to look for this process name being run on our system, we could use code like this:

use Proc::ProcessTable;

my $logfile = 'eggdrops';
open my $LOG, '>>', $logfile or die "Can't open logfile for append:$!\n";

my $t = new Proc::ProcessTable;

foreach my $p ( @{ $t->table } ) {
    if ( $p->fname() =~ /eggdrop/i ) {
        print $LOG time . "\t"
          . getpwuid( $p->uid ) . "\t"
          . $p->fname() . "\n";
    }
}
close $LOG;

If you’re thinking, “This code isn’t good enough! All someone has to do is rename the eggdrop executable to evade its check,” you’re absolutely right. We’ll take a stab at writing some less naïve bot-check code in the very last section of this chapter.

In the meantime, let’s take a look at one more example where Perl assists us in managing user processes. So far all of our examples have been fairly negative, focusing on dealing with resource-hungry and naughty processes. Let’s look at something with a sunnier disposition.

There are times when a system administrator needs to know which (legitimate) programs users on a system are using. Sometimes this is necessary in the context of software metering, where there are legal concerns about the number of users running a program concurrently. In those cases there is usually a licensing mechanism in place to handle the bean counting. Another situation where this knowledge comes in handy is that of machine migration. If you are migrating a user population from one architecture to another, you’ll want to make sure all the programs used on the previous architecture are available on the new one.

One approach to solving this problem involves replacing every non-OS binary available to users with a wrapper that first records that a particular binary has been run and then actually runs it. This can be difficult to implement if there are a large number of binaries. It also has the unpleasant side effect of slowing down every program invocation.

If precision is not important and a rough estimate of which binaries are in use will suffice, we can use Proc::ProcessTable to solve this problem. Here’s some code that wakes up every five minutes and surveys the current process landscape. It keeps a simple count of all of the process names it finds, and it’s smart enough not to count processes it saw during its last period of wakefulness. Every hour it prints its findings and starts collecting again. We wait five minutes between each run because walking the process table is usually a resource-intensive operation, and we’d prefer this program to add as little load to the system as possible:

use Proc::ProcessTable;

my $interval   = 300;    # sleep interval of 5 minutes
my $partofhour = 0;      # keep track of where in the hour we are

my $tobj = new Proc::ProcessTable;    # create new process object

my %last;          # to keep track of info from the previous run
my %current;       # to keep track of data from the current run
my %collection;    # to keep track of info over the entire hour

# forever loop, collecting stats every $interval secs
# and dumping them once an hour
while (1) {
    foreach my $process ( @{ $tobj->table } ) {

        # we should ignore ourselves
        next if ( $process->pid() == $$ );

        # save this process info for our next run
        # (note: this assumes that your PIDs won't recycle between runs,
        #  but on a very busy system that may not be the case)
        $current{ $process->pid() } = $process->fname();

        # ignore this process if we saw it during the last iteration
        next if ( $last{ $process->pid() } eq $process->fname() );

        # else, remember it
        $collection{ $process->fname() }++;
    }

    $partofhour += $interval;
    %last    = %current;
    %current = ();
    if ( $partofhour >= 3600 ) {
        print scalar localtime(time) . ( '-' x 50 ) . "\n";
        print "Name\t\tCount\n";
        print "--------------\t\t-----\n";
        foreach my $name ( sort reverse_value_sort keys %collection ) {
            print "$name\t\t$collection{$name}\n";
        }
        %collection = ();
        $partofhour = 0;
    }
    sleep($interval);
}

# (reverse) sort by values in %collection and by key name
sub reverse_value_sort {
    return $collection{$b} <=> $collection{$a} || $a cmp $b;
}

There are many ways this program could be enhanced. It could track processes on a per-user basis (i.e., only recording one instance of a program launch per user), collect daily stats, present its information as a nice bar graph, and so on. It’s up to you where you might want to take it.

File and Network Operations

For the last section of this chapter, we’re going to lump two of the user action domains together. The processes we’ve just spent so much time controlling do more than just suck up CPU and memory resources; they also perform operations on filesystems and communicate on a network on behalf of users. User administration requires that we deal with these second-order effects as well.

Our focus in this section will be fairly narrow. We’re only interested in looking at file and network operations that other users are performing on a system. We’re also only going to focus on those operations that we can track back to a specific user (or a specific process run by a specific user). With these blinders in mind, let’s go forth.

Tracking File Operations on Windows

If we want to track other users’ open files, the closest we can come involves using a former third-party command-line program called handle, written by Mark Russinovich (formerly of Sysinternals). See the references section at the end of this chapter for information on where to get it. handle can show us all of the open handles on a particular system. Here’s an excerpt from some sample output:

System pid: 4 NT AUTHORITY\SYSTEM
   7C: File  (-W-)   C:\pagefile.sys
  5DC: File  (---)   C:\Documents and Settings\LocalService\Local Settings\
                      Application Data\Microsoft\Windows\UsrClass.dat
  5E0: File  (---)   C:\WINDOWS\system32\config\SAM.LOG
  5E4: File  (---)   C:\Documents and Settings\LocalService\NTUSER.DAT
  5E8: File  (---)   C:\WINDOWS\system32\config\system
  5EC: File  (---)   C:\WINDOWS\system32\config\software.LOG
  5F0: File  (---)   C:\WINDOWS\system32\config\software
  5F8: File  (---)   C:\WINDOWS\system32\config\SECURITY
  5FC: File  (---)   C:\WINDOWS\system32\config\default
  600: File  (---)   C:\WINDOWS\system32\config\SECURITY.LOG
  604: File  (---)   C:\WINDOWS\system32\config\default.LOG
  60C: File  (---)   C:\WINDOWS\system32\config\SAM
  610: File  (---)   C:\WINDOWS\system32\config\system.LOG
  614: File  (---)   C:\Documents and Settings\NetworkService\NTUSER.DAT
  8E0: File  (---)   C:\Documents and Settings\dNb\Local Settings\Application
                      Data\Microsoft\Windows\UsrClass.dat.LOG
  8E4: File  (---)   C:\Documents and Settings\dNb\Local Settings\Application
                      Data\Microsoft\Windows\UsrClass.dat
  8E8: File  (---)   C:\Documents and Settings\dNb\NTUSER.DAT.LOG
  8EC: File  (---)   C:\Documents and Settings\dNb\NTUSER.DAT
  B08: File  (RW-)   C:\Program Files\Symantec AntiVirus\SAVRT
  B3C: File  (R--)   C:\System Volume Information\_restore{96B84597-8A49-41EE-
                      8303-02D3AD2B3BA4}\RP80\change.log
  B78: File  (R--)   C:\Program Files\Symantec AntiVirus\SAVRT\0608NAV~.TMP
------------------------------------------------------------------------------
smss.exe pid: 436 NT AUTHORITY\SYSTEM
    8: File  (RW-)   C:\WINDOWS
   1C: File  (RW-)   C:\WINDOWS\system32

You can also request information on specific files or directories:

> handle.exe c:\WINDOWS\system32\config

Handle v3.3
Copyright (C) 1997-2007 Mark Russinovich
Sysinternals - www.sysinternals.com

System             pid: 4       5E0: C:\WINDOWS\system32\config\SAM.LOG
System             pid: 4       5E8: C:\WINDOWS\system32\config\system
System             pid: 4       5EC: C:\WINDOWS\system32\config\software.LOG
System             pid: 4       5F0: C:\WINDOWS\system32\config\software
System             pid: 4       5F8: C:\WINDOWS\system32\config\SECURITY
System             pid: 4       5FC: C:\WINDOWS\system32\config\default
System             pid: 4       600: C:\WINDOWS\system32\config\SECURITY.LOG
System             pid: 4       604: C:\WINDOWS\system32\config\default.LOG
System             pid: 4       60C: C:\WINDOWS\system32\config\SAM
System             pid: 4       610: C:\WINDOWS\system32\config\system.LOG
services.exe       pid: 552     2A4: C:\WINDOWS\system32\config\AppEvent.Evt
services.exe       pid: 552     2B4: C:\WINDOWS\system32\config\Internet.evt
services.exe       pid: 552     2C4: C:\WINDOWS\system32\config\SecEvent.Evt
services.exe       pid: 552     2D4: C:\WINDOWS\system32\config\SysEvent.Evt
svchost.exe        pid: 848    17DC: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
ccSetMgr.exe       pid: 1172    2EC: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
ccEvtMgr.exe       pid: 1200    23C: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
Rtvscan.exe        pid: 1560    454: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My

handle can provide this information for a specific process name using the -p switch.

Using this executable from Perl is straightforward, so we won’t provide any sample code. Instead, let’s look at a related and more interesting operation: auditing.

Windows allows us to efficiently watch a file, directory, or hierarchy of directories for changes. You could imagine repeatedly performing stat()s on the desired object or objects, but that would be highly CPU-intensive. Under Windows, we can ask the operating system to keep watch for us.

There is a specialized Perl module that makes this job relatively painless for us: Win32::ChangeNotify by Christopher J. Madsen. There is also a related helper module: Win32::FileNotify by Renee Baecker.

Win32::ChangeNotify is pretty easy to use, but it does have one gotcha. The module uses the Win32 APIs to ask the OS to let you know if something changes in a directory. You can even specify what kind of change to look for (last write time, file or directory names/sizes, etc.). The problem is that if you ask it to watch a directory for changes, it can tell you when something changes, but not what has changed. It’s up to the program author to determine that with some separate code. That’s where Win32::FileNotify comes in. If you just need to watch a single file, Win32::FileNotify will go the extra step of double-checking whether the change the OS reported is in the file being audited.

Because they’re so small, we’ll look at examples of both modules. We’ll start with the specific case of watching to see if a file has changed:

use Win32::FileNotify;

my $file = 'c:\windows\temp\importantfile';

my $fnot = Win32::FileNotify->new($file);

$fnot->wait();    # at this point, our program blocks until $file changes

... # go do something about the file change

And here’s some code to look for changes in a directory (specifically, files coming and going):

use Win32::ChangeNotify;

my $dir = 'c:\importantdir';

# watch this directory (second argument says don't watch for changes
# to subdirectories) for changes in the filenames found there
my $cnot = Win32::ChangeNotify->new( $dir, 0, 'FILE_NAME' );

while (1) {

    # blocks for 10 secs (10,000 milliseconds) or until a change takes place
    my $waitresult = $cnot->wait(10000);

    if ( $waitresult == 1 ) {

        ... # call or include some other code here to figure out what changed

        # reset the ChangeNotification object so we can continue monitoring
        $cnot->reset;
    }
    elsif ( $waitresult == 0 ) {
        print "no changes to $dir in the last 10 seconds\n";
    }
    elsif ( $waitresult == −1 ) {
        print "something went blooey in the monitoring\n";
        last;
    }
}

Tracking Network Operations on Windows

That was filesystem monitoring. What about network access monitoring? There are two fairly easy ways to track network operations under Windows. Ideally, as an administrator you’d like to know which process (and therefore which user) has opened a network port. While I know of no Perl module that can perform this task, there are at least two command-line tools that provide the information in a way that could be consumed by a Perl program. The first, netstat, actually ships with the system, but very few people know it can do this (I certainly didn’t for a long time). Here’s some sample output:

> netstat -ano

Active Connections

  Proto  Local Address          Foreign Address        State           PID
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       932
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    127.0.0.1:1028         0.0.0.0:0              LISTENING       1216
  TCP    192.168.16.129:139     0.0.0.0:0              LISTENING       4
  UDP    0.0.0.0:445            *:*                                    4
  UDP    0.0.0.0:500            *:*                                    680
  UDP    0.0.0.0:1036           *:*                                    1068
  UDP    0.0.0.0:1263           *:*                                    1068
  UDP    0.0.0.0:4500           *:*                                    680
  UDP    127.0.0.1:123          *:*                                    1024
  UDP    127.0.0.1:1900         *:*                                    1108
  UDP    192.168.16.129:123     *:*                                    1024
  UDP    192.168.16.129:137     *:*                                    4
  UDP    192.168.16.129:138     *:*                                    4
  UDP    192.168.16.129:1900    *:*                                    1108

The second is another tool from Mark Russinovich, formerly of Sysinternals: TcpView (or more precisely, the tcpvcon utility that comes in that package). It has the nice property of being able to output the information in CSV form, like so:

> tcpvcon -anc

TCPView v2.51 - TCP/UDP endpoint viewer
Copyright (C) 1998-2007 Mark Russinovich
Sysinternals - www.sysinternals.com

TCP,alg.exe,1216,LISTENING,127.0.0.1:1028,0.0.0.0:0
TCP,System,4,LISTENING,0.0.0.0:445,0.0.0.0:0
TCP,svchost.exe,932,LISTENING,0.0.0.0:135,0.0.0.0:0
TCP,System,4,LISTENING,192.168.16.129:139,0.0.0.0:0
UDP,svchost.exe,1024,*,192.168.16.129:123,*:*
UDP,lsass.exe,680,*,0.0.0.0:500,*:*
UDP,svchost.exe,1068,*,0.0.0.0:1036,*:*
UDP,svchost.exe,1108,*,192.168.16.129:1900,*:*
UDP,svchost.exe,1024,*,127.0.0.1:123,*:*
UDP,System,4,*,192.168.16.129:137,*:*
UDP,svchost.exe,1108,*,127.0.0.1:1900,*:*
UDP,lsass.exe,680,*,0.0.0.0:4500,*:*
UDP,System,4,*,192.168.16.129:138,*:*
UDP,svchost.exe,1068,*,0.0.0.0:1263,*:*
UDP,System,4,*,0.0.0.0:445,*:*

This would be trivial to parse with something like Text::CSV::Simple or Text::CSV_XS.

Let’s see how we’d perform the same tasks within the Unix world.

Tracking File and Network Operations in Unix

To handle the tracking of both file and network operations in Unix, we can use a single approach.[25] This is one of few times in this book where calling a separate executable is clearly the superior method. Vic Abell has given an amazing gift to the system administration world by writing and maintaining a program called lsof (LiSt Open Files) that can be found at ftp://vic.cc.purdue.edu/pub/tools/unix/lsof. lsof can show in detail all of the currently open files and network connections on a Unix machine. One of the things that makes it truly amazing is its portability. The latest version as of this writing runs on at least nine flavors of Unix (the previous version supported an even wider variety of Unix flavors) and supports several OS versions for each flavor.

Here’s a snippet of lsof’s output, showing an excerpt of the output for one of the processes I am running. lsof tends to output very long lines, so I’ve inserted a blank line between each line of output to make the distinctions clear:

COMMAND     PID USER   FD   TYPE        DEVICE  SIZE/OFF     NODE NAME
firefox-b 27189  dnb  cwd   VDIR     318,16168     36864 25760428 /home/dnb

firefox-b 27189  dnb  txt   VREG     318,37181    177864  6320643
 /net/csw (fileserver:/vol/systems/csw)

firefox-b 27189  dnb  txt   VREG         136,0     56874     3680
 /usr/openwin/lib/X11/fonts/Type1/outline/Helvetica-Bold.pfa

firefox-b 27189  dnb  txt   VREG     318,37181     16524   563516
 /net/csw (fileserver:/vol/systems/csw)

firefox-b 27189  dnb    0u  unix        105,43       0t0     3352
 /devices/pseudo/tl@0:ticots->(socketpair: 0x1409) (0x300034a1010)

firefox-b 27189  dnb    2u  unix        105,45       0t0     3352
 /devices/pseudo/tl@0:ticots->(socketpair: 0x140b) (0x300034a01d0)

firefox-b 27189  dnb    4u  IPv6 0x3000349cde0 0t2121076
      TCP localhost:32887->localhost:6010 (ESTABLISHED)

firefox-b 27189  dnb    6u  FIFO 0x30003726ee8       0t0  2105883
 (fifofs) ->0x30003726de0

firefox-b 27189  dnb   24r  VREG     318,37181    332618
    85700 /net/csw (fileserver:/vol/systems/csw)

firefox-b 27189  dnb   29u  unix        105,46    0t1742
     3352 /devices/pseudo/tl@0:ticots->/var/tmp/orbit-dnb/linc
-6a37-0-47776fee636a2 (0x30003cc1900->0x300045731f8)

firefox-b 27189  dnb   31u  unix        105,50       0t0
     3352 /devices/pseudo/tl@0:ticots->/var/tmp/orbit-dnb/linc
-6a35-0-47772fb086240 (0x300034a13a0)

firefox-b 27189  dnb   43u  IPv4 0x30742eb79b0   0t42210
      TCP desktop.example.edu:32897->images.slashdot.org:www (ESTABLISHED)

This output demonstrates some of the power of this command. It shows the current working directory (VDIR), regular files (VREG), pipes (FIFO), and network connections (IPv4/IPv6) opened by this process.

The easiest way to use lsof from Perl is to invoke its special “field” mode (-F). In this mode, its output is broken up into specially labeled and delimited fields, instead of the ps-like columns just shown. This makes parsing the output a cinch.

There is one quirk to the field mode output. It is organized into what the author calls “process sets” and “file sets.” A process set is a set of field entries referring to a single process, and a file set is a similar set for a file. This all makes more sense if we turn on field mode with the 0 option. Fields are then delimited with NUL (ASCII 0) characters, and sets with NL (ASCII 12) characters. Here’s a similar group of lines to those in the preceding output, this time in field mode (NUL is represented as ^@). I’ve added spaces between the lines again to make it easier to read:

p27189^@g27155^@R27183^@cfirefox-bin^@u6070^@Ldnb^@
fcwd^@a ^@l

^@tVDIR^@N0x30001b7b1d8^@D0x13e00003f28^@s36864^@i25760428^@k90^@n/home/dnb^@
ftxt^@a ^@l

^@tVREG^@N0x3000224a0f0^@D0x13e0000913d^@s177864^@i6320
643^@k1^@n/net/csw (fileserver:/vol/systems/csw)^@
ftxt^@a ^@l

^@tVREG^@N0x30001714950^@D0x8800000000^@s35064^@i2800^@k1^@n/usr/lib/nss_files.so.1

^@tVREG^@N0x300036226c0^@D0x8800000000^@s56874^@i3680^@k1^@n/usr/
openwin/lib/X11/fonts/Type1/outline/Helvetica-Bold.pfa^@
ftxt^@a ^@l

^@tunix^@F0x3000328c550^@C6^@G0x3;0x0^@N0x300034a1010^@D0x8800
000000^@o0t0^@i3352^@n/devices/pseudo/tl@0:ticots->(socketpair:
 0x1409) (0x300034a1010)^@
f1^@au^@l

^@tDOOR^@F0x3000328cf98^@C1^@G0x2001;0x1^@N0x3000178b300^@D0x13
c00000000^@o0t0^@i54^@k27^@n/var/run (swap) (door to nscd[240])^@
f4^@au^@l

^@tIPv6^@F0x300037258f0^@C1^@G0x83;0x1^@N0x300034ace50^@d0x3000349
cde0^@o0t3919884^@PTCP^@nlocalhost:32887->localhost:6010^@TST=
ESTABLISHED^@TQR=0^@TQS=8191^@TWR=49152^@TWW=13264^@
f5^@au^@l

^@tFIFO^@F0x30003724f50^@C1^@G0x3;0x0^@N0x30003726de0^@d0x30003726
de0^@o0t0^@i2105883^@n(fifofs) ->0x30003726ee8^@
f6^@au^@l

^@tFIFO^@F0x30003725420^@C1^@G0x3;0x0^@N0x30003726ee8^@d0x30003726
ee8^@o0t0^@i2105883^@n(fifofs) ->0x30003726de0^@
f7^@aw^@lW^@tVREG^@F0x30003724c40^@C1^@G0x302;0x0^@N0x30001eadbf8^
@D0x13e00003f28^@s0^@i1539532^@k1^@n/home/dnb (fileserver:/vol/homedirs/systems/dnb)^@
f8^@au^@l

^@tIPv4^@F0x30003724ce8^@C1^@G0x83;0x0^@N0x300034ac010^@d0x
300040604f0^@o0t4094^@PTCP^@ndesktop.example.edu:32931->web
-vip.srv.jobthread.com:www^@TST=CLOSE_WAIT^@TQR=0^@TQS=0^@TWR=49640^@TWW=6960^@
f44^@au^@l

^@tVREG^@F0x3000328c5c0^@C1^@G0x2103;0x0^@N0x300051cd3f8^@
D0x13e00003f28^@s276^@i16547341^@k1^@n/home/dnb (fileserver:/vol/
homedirs/systems/dnb)^@
f45^@au^@l

^@tVREG^@F0x30003725f80^@C1^@G0x3;0x0^@N0x300026ad920^@D0x
13e00003f28^@s8468^@i21298675^@k1^@n/home/dnb (fileserver:/vol/homedirs/systems/dnb)^@
f46^@au^@l

^@tIPv4^@F0x30003724a10^@C1^@G0x83;0x0^@N0x309ab62b578^@d0x30742
eb76b0^@o0t20726^@PTCP^@ndesktop.example.edu:32934->216.66.26.
161:www^@TST=ESTABLISHED^@TQR=0^@TQS=0^@TWR=49640^@TWW=6432^@
f47^@au^@l

^@tVREG^@F0x3000328c080^@C1^@G0x2103;0x0^@N0x30002186098^@D0x
13e00003f28^@s66560^@i16547342^@k1^@n/home/dnb (fileserver:/vol/
homedirs/systems/dnb)^@
f48^@au^@l

Let’s deconstruct this output. The first line is a process set (we can tell because it begins with the letter p):

p27189^@g27155^@R27183^@cfirefox-bin^@u6070^@Ldnb^@
fcwd^@a ^@l

Each field begins with a letter identifying the field’s contents (p for pid, c for command, u for uid, and L for login) and ends with a delimiter character. Together the fields on this line make up a process set. All of the lines that follow, up until the next process set, describe the open files/network connections of the process described by this process set.

Let’s put this mode to use. If we wanted to show all of the open files on a system and the PIDs that are using them, we could use code like this:[26]

use Text::Wrap;

my $lsofexec = '/usr/local/bin/lsof';  # location of lsof executable

# (F)ield mode, NUL (0) delim, show (L)ogin, file (t)ype and file (n)ame
my $lsofflag = '-FL0tn';

open my $LSOFPIPE, '-|', "$lsofexec $lsofflag"
    or die "Unable to start $lsofexec: $!\n";

my $pid;                               # pid as returned by lsof
my $pathname;                          # pathname as returned by lsof
my $login;                             # login name as returned by lsof
my $type;                              # type of open file as returned by lsof
my %seen;                              # for a pathname cache
my %paths;                             # collect the paths as we go

while ( my $lsof = <$LSOFPIPE> ) {

    # deal with a process set
    if ( substr( $lsof, 0, 1 ) eq 'p' ) {
        ( $pid, $login ) = split( /\0/, $lsof );
        $pid = substr( $pid, 1, length($pid) );
    }

    # deal with a file set; note: we are only interested
    # in "regular" files (as per Solaris and Linux, lsof on other
    # systems may mark files and directories differently)
    if ( substr( $lsof, 0, 5 ) eq 'tVREG' or   # Solaris
         substr( $lsof, 0, 4 ) eq 'tREG') {    # Linux
        ( $type, $pathname ) = split( /\0/, $lsof );

        # a process may have the same pathname open twice;
        # these two lines make sure we only record it once
        next if ( $seen{$pathname} eq $pid );
        $seen{$pathname} = $pid;

        $pathname = substr( $pathname, 1, length($pathname) );
        push( @{ $paths{$pathname} }, $pid );
    }
}

close $LSOFPIPE;

foreach my $path ( sort keys %paths ) {
    print "$path:\n";
    print wrap( "\t", "\t", join( " ", @{ $paths{$path} } ) ), "\n";
}

This code instructs lsof to show only a few of its possible fields. We iterate through its output, collecting filenames and PIDs in a hash of lists. When we’ve received all of the output, we print the filenames in a nicely formatted PID list (thanks to David Muir Sharnoff’s Text::Wrap module):

/home/dnb (fileserver:/vol/homedirs/systems/dnb):
        12777 12933 27293 28223
/usr/lib/ld.so.1:
        10613 12777 12933 27217 27219 27293 28147 28149 28223 28352 28353
        28361
/usr/lib/libaio.so.1:
        27217 28147 28352 28353 28361
/usr/lib/libc.so.1:
        10613 12777 12933 27217 27219 27293 28147 28149 28223 28352 28353
        28361
/usr/lib/libmd5.so.1:
        10613 27217 28147 28352 28353 28361
/usr/lib/libmp.so.2:
        10613 27217 27219 28147 28149 28352 28353 28361
/usr/lib/libnsl.so.1:
        10613 27217 27219 28147 28149 28352 28353 28361
/usr/lib/libsocket.so.1:
        10613 27217 27219 28147 28149 28352 28353 28361
/usr/lib/sparcv9/libnsl.so.1:
        28362 28365
/usr/lib/sparcv9/libsocket.so.1:
        28362 28365
/usr/platform/sun4u-us3/lib/libc_psr.so.1:
        10613 12777 12933 27217 27219 27293 28147 28149 28223 28352 28353
        28361
/usr/platform/sun4u-us3/lib/sparcv9/libc_psr.so.1:
        28362 28365
...

For our last example of tracking Unix file and network operations, let’s return to an earlier example, where we attempted to find IRC bots running on a system. There are more reliable ways to find network daemons like bots than looking at the process table. A user may be able to hide the name of a bot by renaming the executable, but he’ll have to work a lot harder to hide the open network connection. More often than not, this connection is to a server running on TCP ports 6660–7000. lsof makes looking for these processes easy:

my $lsofexec = '/usr/local/bin/lsof';     # location of lsof executable
my $lsofflag = '-FL0c -iTCP:6660-7000';   # specify ports and other lsof flags

# This is a hash slice being used to preload a hash table, the
# existence of whose keys we'll check later. Usually this gets written
# like this:
#     %approvedclients = ('ircII' => undef, 'xirc' => undef, ...);
# (but this is a cool idiom popularized by Mark-Jason Dominus)
my %approvedclients;
@approvedclients{ 'ircII', 'xirc', 'pirc' } = ();

open my $LSOFPIPE, "$lsofexec $lsofflag|"
    or die "Unable to start $lsofexec:$!\n";

my $pid;
my $command;
my $login;
while ( my $lsof = <$LSOFPIPE> ) {
    ( $pid, $command, $login ) =
                    $lsof =~ /p(\d+)\000
                              c(.+)\000
                              L(\w+)\000/x;
    warn "$login using an unapproved client called $command (pid $pid)!\n"
        unless ( exists $approvedclients{$command} );
}

close $LSOFPIPE;

This is the simplest check we can make. It will catch users who rename eggdrop to something like pine or -tcsh, as well as those users who don’t even attempt to hide their bots. However, it suffers from a similar flaw to our other approach. If a user is smart enough, she may rename her bot to something on our “approved clients” list. To continue our hunt, we could take at least two more steps:

  • Use lsof to check that the file opened for that executable really is the file we expect it to be, and not some random binary in a user filesystem.

  • Use our process control methods to check that the user is running this program from an existing shell. If this is the only process running for a user (i.e., if the user has logged off but left it running), it is probably a daemon and hence a bot.

This cat-and-mouse game brings us to a point that will help wrap up the chapter. In Chapter 3, we mentioned that users are fundamentally unpredictable. They do things system administrators don’t anticipate. There is an old saying: “Nothing is foolproof because fools are so ingenious.” It is important to come to grips with this fact as you program Perl for user administration. You’ll write more robust programs as a result, and when one of your programs goes “blooey” because a user did something unexpected, you’ll be able to sit back calmly and admire the ingenuity.

Module Information for This Chapter

Module

CPAN ID

Version

Text::CSV_XS

HMBRAND

0.32

Win32::Process::Info

WYANT

1.011

Win32::Setupsup

JHELBERG

1.0.1.0

Win32::GuiTest

KARASIK

1.54

Win32::OLE (ships with ActiveState Perl)

JDB

0.1703

Proc::ProcessTable

DURIST

0.41

Data::Dumper (ships with Perl)

GSAR

2.121

Win32::ChangeNotify

JDB

1.05

Win32::FileNotify

RENEEB

0.1

Text::Wrap (ships with Perl)

MUIR

2006.1117

Installing Win32::Setupsup

If you want to install Win32::Setupsup, you’ll need to get it from a different PPM repository than the default one configured when you first installed ActiveState Perl. It can be found (as of this writing) in the very handy supplementary repository maintained by Randy Kobes at the University of Winnipeg. I’d recommend adding this repository even if you don’t plan to use Win32::Setupsup. The easiest way to do this is from the command line, like so:

$ ppm repo add uwinnipeg http://theoryx5.uwinnipeg.ca/ppms/

or, if using Perl 5.10:

$ ppm repo add uwinnipeg http://cpan.uwinnipeg.ca/PPMPackages/10xx/

You can also add it to the GUI version of PPM4 by choosing Preferences in the Edit menu and selecting the Repositories tab. More info about this repository can be found at http://theoryx5.uwinnipeg.ca/ppms/.

References for More Information

http://aspn.activestate.com/ASPN/Mail/ hosts the Perl-Win32-Admin and Perl-Win32-Users mailing lists. Both lists and their archives are invaluable resources for Win32 programmers.

http://www.microsoft.com/whdc/system/pnppwr/wmi/default.mspx is the current home for WMI at Microsoft.com. This address has changed a few times since the first edition, so doing a web search for “WMI” may be a better way to locate the WMI URL du jour at Microsoft.

http://technet.microsoft.com/sysinternals/ is the home (as of this writing) of the handle program and many other valuable Windows utilities that Microsoft acquired when it bought Sysinternals and hired its principals. http://sysinternals.com still exists as of this writing and redirects to the correct Microsoft URL. If you can’t find these utilities in any of Microsoft’s websites, perhaps going to that URL will point you at the current location.

http://www.dmtf.org is the home of the Distributed Management Task Force and a good source for WBEM information.

If you haven’t yet, you must download the Microsoft Scriptomatic tool (version 2 as of this writing) from http://www.microsoft.com/technet/scriptcenter/tools/scripto2.mspx. This Windows tool from “the Microsoft Scripting Guys” lets you poke around the WMI namespaces on your machine. When you find something you might be interested in using, it can write a script to use it for you. Really. But even better than that, it can write the script for you in VBScript, JScript, Perl, or Python. I’m raving about this tool both here and in the other chapters that mention WMI because I like it so much. If you want to use it under Vista, though, be sure to read the section on Vista in Chapter 1.



[19] In the first edition of this book, this section was called “Using the Win32::IProc module.” Win32::IProc shared the fate of the module I describe in the sidebar The Ephemeral Nature of Modules.

[20] As much as Microsoft would like to see these data access mechanisms become ubiquitous, the likelihood of finding them in a non-Win32 environment is slight. This is why I refer to them as “Win32-specific.”

[21] Microsoft provides WQL, a scaled-down query language based on SQL syntax, for this purpose. Once upon a time it also provided ODBC-based access to the data, but that approach has been deprecated in more recent OS releases.

[22] REPL stands for Read-Eval-Print Loop, a term from the LISP (LISt Processing) world. A REPL lets you type code into a prompt, have it be executed by the language’s interpreter, and then review the results.

[23] See the section Active Directory Service Interfaces for details on another prominent use of in.

[24] Later, we’ll look at a module called Proc::ProcessTable that can do this for you without you having to write the code.

[25] This is the best approach for portability. Various OSs have their own mechanisms (inotify, dnotify, etc.), and frameworks like DTrace are very cool. Mac OS X 10.5+ has a similar auditing facility to the one we saw with Windows (Mac::FSEvents gives you easy access to it). However, none of these options is as portable as the approach described here.

[26] If you don’t want to parse lsof’s field mode by hand Marc Beyer’s Unix::Lsof will handle the work for you.

Get Automating System Administration with Perl, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.