[ Team LiB ] Previous Section Next Section

22.3 Enumerating Sessions and Resources

We now want to show you how to use ADSI to do the following:

  • Enumerate a client's sessions and resources

  • Show which users are currently logged on to a server and count all the logged-on users across a domain's PDCs, BDCs, and other servers

Windows NT, Windows 2000, and Windows Server 2003 machines host two kinds of dynamic objects that you can access with ADSI: sessions (i.e., instances of users connected to a computer) and resources (i.e., instances of file or queue access on a computer). When users connect to a file or a share on a computer, that creates both a session and a resource object. When the user disconnects, these dynamic objects cease to exist.

You can access dynamic objects by connecting directly to the Server service on the machine. Although each Server service has a user-friendly display name that appears in the Computer Management console in Windows 2000 and Windows Server 2003 or the Services applet in Control Panel in NT, each Server service also has an ordinary name that you use when connecting to it with ADSI. For example, Server is the display name of the service that has the short name LanManServer. If you enumerate all the services on a machine, you can use IADsService::DisplayName to print the display name and IADs::Name to print the short name.

LanManServer is an object of type FileService. FileService objects are responsible for maintaining the sessions and resources in their jurisdictions. You can use the IADsFileServiceOperations interface to access information about these sessions and resources. This simple interface has two methods: IADsFileServiceOperations::Sessions and IADsFileServiceOperations::Resources. Both methods return collections of objects that you can iterate through with a For Each...Next loop. When you're iterating through a collection in this manner, the system is using IADsCollection::GetObject to retrieve each item from the collection. As a result, you can use the same IADsCollection::GetObject method to retrieve a specific session or resource object. You then can use the IADsSession or IADsResource interface to manipulate that session or resource object's properties to access information. For example, if you retrieve a session object, you can access such information as the username of the user who is logged on and how long that user has been logged on.

22.3.1 Identifying a Machine's Sessions

The following script uses IADsSession to iterate through all the sessions on a particular machine:

On Error Resume Next
   
Dim objComputer, objSession, strOutput
   
strOutput = ""
   
Set objComputer = GetObject("WinNT://mydomainorworkgroup/mycomputer/LanManServer")
   
For Each objSession In objComputer.Sessions
  strOutput = strOutput & "Session Object Name : " & objSession.Name & vbCrLf
  strOutput = strOutput & "Client Computer Name: " & objSession.Computer & vbCrLf
  strOutput = strOutput & "Seconds connected   : " _
    & objSession.ConnectTime & vbCrLf
  strOutput = strOutput & "Seconds idle        : " & objSession.IdleTime & vbCrLf
  strOutput = strOutput & "Connected User      : " & objSession.User & vbCrLf
  strOutput = strOutput & vbCrLf
Next
   
WScript.Echo strOutput

This is straightforward. It uses the IADs::Name property method and IADsSession property methods to retrieve data about the session. The IADs::Name property method displays the object name, which is the name that you would use with IADsCollection::GetObject to retrieve the specific session individually. As Figure 22-1 shows, the object name always follows the format user\COMPUTER. In some sessions, the underlying system rather than a person is connecting to the computer. Here, the object name follows the format \COMPUTER.

Figure 22-1. The sessions on a computer
figs/ads2.2201.gif

You can use IADsSession property methods to retrieve the individual components of the object name. The IADsSession::Computer property method retrieves the computer component (e.g., COMPUTER1). The Connected User and Client Computer Name fields in Figure 22-1 contain the results of these property methods. The IADsSession::User property method retrieves the user component of the object name (e.g., user1).

The next line highlights an important consideration when you're specifying WinNT provider paths in a script:

Set objComputer = GetObject("WinNT://mydomainorworkgroup/mycomputer/LanManServer")

If you use only the computer name in the path with code such as the following, your script will execute slowly because the system must locate the machine and its workgroup:

WinNT://MYCOMPUTER,computer

However, if you include the workgroup in the path, your script will execute significantly faster because the system can immediately access the machine:

WinNT://MYDOMAIN/MYCOMPUTER,computer

22.3.2 Identifying a Machine's Resources

The following script enumerates the resources in use on a machine:

On Error Resume Next
   
Dim objComputer, objSession, strOutput
   
strOutput = ""
   
Set objComputer = GetObject("WinNT://mydomainorworkgroup/mycomputer/LanManServer")
   
For Each objResource In objComputer.Resources
  strOutput = strOutput & "Resource Name: " & objResource.Name & vbCrLf
  strOutput = strOutput & "User         : " & objResource.User & vbCrLf
  strOutput = strOutput & "Path         : " & objResource.Path & vbCrLf
  strOutput = strOutput & "Lock count   : " & objResource.LockCount & vbCrLf
  strOutput = strOutput & vbCrLf
Next
   
Wscript.Echo strOutput

Figure 22-2 shows the output, which lists those files that each user has open. The Microsoft Excel spreadsheet that user3 has open is locked.

Figure 22-2. The resources on a computer
figs/ads2.2202.gif

If you want to see locks in action, have one user open a shared document and have another user then try to open it.

22.3.3 A Utility to Show User Sessions

You can use ADSI to write a script that displays which users are currently logged on to a server and counts all the logged-on users across a domain. For simplicity, suppose that you have only two servers in your domain. You want to determine and display the maximum number of simultaneous sessions on each server, the total number of sessions across the domain, the total number of unique connected users on the domain, and an alphabetized list of usernames. Users can simultaneously connect to both servers from their computers. However, you want to count these users only once.

You can use the session object to construct ShowUsers.vbs, a useful utility that runs from your desktop and displays this user session information. What follows is an overview of how the ShowUsers.vbs utility obtains, manipulates, and displays the data. We heavily commented this script to show how it works line by line. The script follows the discussion of how it works.

22.3.3.1 Obtaining the data

ShowUsers.vbs begins by iterating through all your servers. To specify the servers you want to scan, you can either hardcode the server information in the script or have the script dynamically retrieve this information.[2] We've hardcoded the server information in ShowUsers.vbs.

[2] For Active Directory, a domain's DCs are always in the Domain Controllers Organizational Unit off the root of the domain. Member servers will be in the Computers container by default.

When the utility iterates through all the servers, it ignores any empty usernames (which specify interserver connections) and usernames with a trailing dollar sign (which denote users that are actually computers connecting to one another). For each valid session, the script records the username and increments the session count by one.

The script uses a dynamic array (arrResults) to store the username data because the number of usernames in the array will change each time you run the utility.

The script uses a multidimensional array (arrServerResults) to store the servers' names and maximum number of connected sessions:

'**********************************************************************
'Sets up multidimensional array to hold server names and user counts
'**********************************************************************
arrServerResults(0,0) = "server1"
arrServerResults(1,0) = "server2"
arrServerResults(0,1) = 0 
arrServerResults(1,1) = 0

The arrServerResults array stores this information in a simple table, putting the server names in the first column, the counts in the second column, and the data in the rows. To access data in arrServerResults, we include the indexes of first and second dimensions, respectively, in parentheses. For example, arrServerResults(0,1) accesses the data in the first row (0), second column (1). Thus, the server names are in arrServerResults(0,0) and arrServerResults(1,0). The corresponding session counts are in arrServerResults(0,1) and arrServerResults(1,1).

The script can iterate through the servers by using a For loop to go from 0 to UBound(arrServerResults). The VBScript UBound function retrieves the upper array bound for an array and takes two parameters: the array to check and the dimension to count the upper bound of.

Note that UBound's second parameter specifying the dimension starts from 1, not 0 as the actual array does.

If the second parameter is left off, the first dimension is used; these are equivalent:

UBound(arrServerResults,1)
UBound(arrServerResults)
22.3.3.2 Manipulating the data

After the script iterates through every server, you have a list of server session counts and a list of the usernames of those users who were connected to the servers at that time. This section of code achieves this:

For Each objSession In objFSO.Sessions
  If (Not objSession.User = "") And (Not Right(objSession.User,1) = "$") Then
    arrResults(UBound(arrResults)) = objSession.User & vbCrLf
    ReDim Preserve arrResults(UBound(arrResults) + 1)
    arrServerResults(intIndex,1) = arrServerResults(intIndex,1) + 1
  End If
Next

Note the use of ReDim to preserve the existing contents of the array, while expanding the array's upper bound by one. If the upper bound is 12, you can increase the array to 13 elements with the following code:

ReDim Preserve arrResults(13)

Using the upper bound of the existing array as the parameter makes the code generic. The following line is used to increase the count of the users by one in the second dimension of the array:

arrServerResults(intIndex,1) = arrServerResults(intIndex,1) + 1

Because some users might have been connected to both servers and hence might appear in the username list twice, the script uses two subprocedures to manipulate the data. One subprocedure sorts the usernames; the other subprocedure removes duplicate usernames.

22.3.3.3 The sort subprocedure

You likely remember from your college days having to perform bubble sorts and shell sorts. Although including in VBScript a general-purpose quick sort like the bubble or shell sort would've made sense, Microsoft failed to do so.

The Quicksort subprocedure we use in the next example takes in an array indexed from 0 to UBound(array) and sorts the values in the array between the two indexes you pass in as arguments. For example, if you specify the following code, Quicksort sorts elements 7 through 19 in arrMyArray:

Quicksort(arrMyArray, 7, 19)

Quicksort's own subprocedure has the ability to sort between two indexes in case we ever want to reuse the procedure in another script that needs that functionality. However, in ShowUsers.vbs, we need to sort the whole array of usernames between indexes 0 and UBound(array).

22.3.3.4 The duplicate-removal subprocedure

After Quicksort sorts the username list, the RemoveDuplicates subprocedure removes any duplicate usernames. Like Quicksort, RemoveDuplicates takes an array and two indexes as arguments.[3]

[3] When we created this subprocedure, we gave it the ability to work between two indexes so that RemoveDuplicates and Quicksort are comparable.

When RemoveDuplicates enumerates through a sorted list, it ignores items with the same name as the next item in the list and then passes the remaining elements to a new array. For example, let's say that a sorted list reads:

bill, bill, bill, sandy, sandy, steve

RemoveDuplicates reads the list as:

<ignore>, <ignore>, bill, <ignore>, sandy, steve

This enumerates to:

bill, sandy, steve

RemoveDuplicates then passes the remaining elements to a new array because placing the results into a second array is faster than manipulating the existing array.

22.3.3.5 Displaying the data

Here are the constants we use for setting the location of the temporary file and opening it for write:

Const TEMPFILE = "C:\SHOWUSERS-TEMP.TXT"
Const ForWriting = 2

We then open the temporary text file for writing by creating a FileSystemObject and using the FileSystemObject::OpenTextFile method to open it. The third parameter states that if the text file already exists, it should be overwritten:

Set fso = CreateObject("Scripting.FileSystemObject")
Set ts = fso.OpenTextFile(TEMPFILE, ForWriting, True)

We then use the TextStream::WriteLine and TextStream::Write functions to write the data to the file and ultimately use the TextStream::Close method to close it.

Having the file written is only half the battle. We now want to display the file automatically using Notepad, maximize the window, display the results, and delete the file, again automatically, when we close Notepad. This is actually accomplished simply, as follows:

Set objShell = CreateObject("WScript.Shell")
intRC = objShell.Run ("notepad.exe " & TEMPFILE, vbMaximizedFocus, TRUE)
fso.DeleteFile(TEMPFILE)

The Shell::Run method allows you to open and use an application such as Notepad synchronously or asynchronously with a script. The first parameter uses the temporary file as a parameter to Notepad, so that Notepad opens the file. The second parameter is one of the VBScript constants that came up before while setting rights to home directories for newly created users. The third parameter indicates whether to run the command synchronously (true) or asynchronously (false). In this case, the script pauses execution when Notepad is open and doesn't start up again until you close Notepad. The script effectively stops executing until you close Notepad. When that happens, a return code is placed into the variable intRC. This is to accommodate applications and commands that return a value that you may require. In this case, you don't care about a value being returned, so when Notepad is closed, the script deletes the file.

The full code for ShowUsers is listed in Example 22-1.

Example 22-1. The ShowUsers.vbs utility
'**********************************************************************
'The ShowUsers.vbs Utility
'**********************************************************************
Option Explicit
   
On Error Resume Next
   
'**********************************************************************
'Maximizes the Notepad screen when started
'**********************************************************************
Const vbMaximizedFocus   = 3
   
'**********************************************************************
'The domain or workgroup in which the servers or workstations reside
'**********************************************************************
Const strDomainOrWorkGroupName = "MYDOMAIN"
   
'**********************************************************************
'Sets the location of the temporary file
'**********************************************************************
Const TEMPFILE = "C:\SHOWUSERS-TEMP.TXT"
   
'**********************************************************************
'Opens a file and lets you start writing from the beginning of the
'file
'**********************************************************************
Const ForWriting = 2
   
'**********************************************************************
'Declare all variables. As arrResults will be continually increased 
'in size as more results are fed in, you have to initially declare it 
'as an unbounded array
'**********************************************************************
Dim objShell, objFSO, objSession, arrServerResults(1,1), arrResults(  )
Dim arrResults2(  ), fso, ts, intRC, intMaxSessions, intIndex, strItem
   
'**********************************************************************
'Sets up multidimensional array to hold server names and user counts
'**********************************************************************
arrServerResults(0,0) = "server1"
arrServerResults(1,0) = "server2"
arrServerResults(0,1) = 0 
arrServerResults(1,1) = 0
   
'**********************************************************************
'Redimensions arrResults to one element to start with
'**********************************************************************
ReDim arrResults(0)
   
'**********************************************************************
'Iterates through the array, connecting to the server service of
'each server and looks at each session on that server
'
'If the session has an empty user (is an interserver connection) or 
'the user is a computer (the trailing character is a dollar sign), the
'script ignores that session and proceeds to the next session
'
'If the session is valid, the script adds the username to the last
'element of the arrResults array and expands the array by one element 
'to cope with the next result when it arrives. The script also
'increments the session count for the corresponding server by one
'**********************************************************************
For intIndex = 0 To UBound(arrServerResults)
  Set objFSO = GetObject("WinNT://" & strDomainOrWorkGroupName & "/" _
    & arrServerResults(intIndex,0) & "/LanmanServer")
   
  For Each objSession In objFSO.Sessions
    If (Not objSession.User = "") And (Not Right(objSession.User,1) = "$") Then
      arrResults(UBound(arrResults)) = objSession.User & vbCrLf
      ReDim Preserve arrResults(UBound(arrResults) + 1)
      ArrServerResults(intIndex,1) = arrServerResults(intIndex,1) + 1
    End If
  Next
   
  Set objFSO = Nothing
Next
   
'**********************************************************************
'Sorts the entire arrResults array and then removes duplicates from 
'it, placing the results in arrResults2
'**********************************************************************
Quicksort arrResults, 0, UBound(arrResults)
RemoveDuplicates arrResults, 0, UBound(arrResults), arrResults2
   
'**********************************************************************
'Opens the temporary text file for writing. If the text file already 
'exists, overwrite it.
'**********************************************************************
Set fso = CreateObject("Scripting.FileSystemObject")
Set ts = fso.OpenTextFile(TEMPFILE, ForWriting, True)
   
'**********************************************************************
'Counts the max sessions by iterating through each server and adding
'up the sessions count in the second column of each row of the 
'multidimensional array
'
'Writes out the user sessions for each server to the temporary file 
'as the script iterates through the list. When the script finishes 
'counting, it writes out the max sessions to the file as well.
'**********************************************************************
intMaxSessions = 0
For intIndex = 0 To UBound(arrServerResults)
  ts.WriteLine "Total User Sessions on " & arrServerResults(intIndex,0) _
    & ": " & arrServerResults(intIndex,1)
  intMaxSessions = intMaxSessions + arrServerResults(intIndex,1)
Next
ts.WriteLine "Total User sessions on CFS: " & intMaxSessions
ts.WriteLine
   
'**********************************************************************
'Writes out the total number of unique users connected to the domain,
'followed by each username in alphabetic order
'**********************************************************************
ts.WriteLine "Total Users on CFS: " & UBound(arrResults2)
ts.WriteLine
For Each strItem in arrResults2
  ts.Write strItem
Next
ts.Close
   
'**********************************************************************
'Sets the third parameter of the Shell::Run method to TRUE, which
'allows the script to open up the file in Notepad and maximize the
'screen. The script stops executing until you close Notepad, which 
'places a return code into intRC. When Notepad is closed, the script
'deletes the file.
'**********************************************************************
Set objShell = CreateObject("WScript.Shell")
intRC = objShell.Run ("notepad.exe " & TEMPFILE, vbMaximizedFocus, TRUE)
fso.DeleteFile(TEMPFILE)
   
'**********************************************************************
'Subroutine Quicksort
'
'Sorts the items in the array (between the two values you pass in)
'**********************************************************************
Sub Quicksort(strValues(  ), ByVal min, ByVal max)
   
  Dim strMediumValue, high, low, i
   
  '**********************************************************************
  'If the list has only 1 item, it's sorted
  '**********************************************************************
  If min >= max Then Exit Sub
   
  '**********************************************************************
  'Pick a dividing item randomly
  '**********************************************************************
  we = min + Int(Rnd(max - min + 1))
  strMediumValue = strValues(i)
   
  '**********************************************************************
  'Swap the dividing item to the front of the list
  '**********************************************************************
  strValues(i) = strValues(min)
   
  '**********************************************************************
  'Separate the list into sublists
  '**********************************************************************
  low = min
  high = max
  Do
    '**********************************************************************
    'Look down from high for a value < strMediumValue
    '**********************************************************************
    Do While strValues(high) >= strMediumValue
      high = high - 1
      If high <= low Then Exit Do
    Loop
   
    If high <= low Then
      '**********************************************************************
      'The list is separated
      '**********************************************************************
      strValues(low) = strMediumValue
      Exit Do
    End If
   
    '**********************************************************************
    'Swap the low and high strValues
    '**********************************************************************
    strValues(low) = strValues(high)
   
    '**********************************************************************
    'Look up from low for a value >= strMediumValue
    '**********************************************************************
    low = low + 1
    Do While strValues(low) < strMediumValue
      low = low + 1
      If low >= high Then Exit Do
    Loop
   
    If low >= high Then
      '**********************************************************************
      'The list is separated
      '**********************************************************************
      low = high
      strValues(high) = strMediumValue
      Exit Do
    End If
   
    '**********************************************************************
    'Swap the low and high strValues
    '**********************************************************************
    strValues(high) = strValues(low)
  Loop 'Loop until the list is separated.
   
  '**********************************************************************
  'Recursively sort the sublists
  '**********************************************************************
  Quicksort strValues, min, low - 1
  Quicksort strValues, low + 1, max
   
End Sub
   
'**********************************************************************
'Subroutine RemoveDuplicates
'
'Removes duplicate items in the strValues array (between the two 
'values you pass in) and writes the results to strNewValues(  )
'**********************************************************************
Sub RemoveDuplicates(ByVal strValues(  ), ByVal min, ByVal max, strNewValues(  ) )
   
  Dim strValuesIndex, strNewValuesIndex
   
  ReDim strNewValues(0)
  strNewValuesIndex = 0
   
  For strValuesIndex = min To max-1
    If Not strValues(strValuesIndex) = strValues(strValuesIndex+1) Then
      strNewValues(strNewValuesIndex) = strValues(strValuesIndex)
      ReDim Preserve strNewValues(strNewValuesIndex + 1)
      strNewValuesIndex = strNewValuesIndex + 1
    End If
  Next
  strNewValues(strNewValuesIndex) = strValues(max)
   
End Sub
22.3.3.6 Room for improvement

Although ShowUsers.vbs is useful, this utility is lacking in one area: users can legitimately use two connection slots if their IADsSession::Computer names are different, but the utility counts the user only once. For example, user1 might log on to the domain twice, once on COMPUTER1 and once on SERVER1, but our script counts user1 only once because of the RemoveDuplicates subprocedure. If you want to make the script even better, you can create an extension to the utility that remedies this situation. For example, the extension might log all user counts to a file every five minutes for later analysis.

    [ Team LiB ] Previous Section Next Section