The Server Side of File Notification


The File Notification Server is a slow EXE server, not a fast DLL server. Why? Because a DLL server would be too efficient. You want the server code to run in a separate thread, and, in Visual Basic, putting it in a separate EXE is the best way to do that. Performance isn’t really an issue with file notification. The more important issue is making sure notifications don’t slow down normal operation of the client.


The Connect method is what launches each file notification thread, but before we examine it, let’s look at some of the data it uses. The server maintains an array of notification handles. In addition, it must keep a parallel array of the data associated with each handle. Later you’ll see why the handles need to be in a separate array. For each notification, the server maintains four pieces of information: the directory to be watched, the type of event, a flag indicating whether to check subdirectories, and the IFileNotifier object to be called when a file event occurs. The data looks like this:

Public Type TConnection
sDir As String
efn As EFILE_NOTIFY
fSubTree As Boolean
notifier As IFileNotifier
End Type

' Actually cLastNotify + 1 allowed
Public Const cLastNotify = 28
' One extra blank item in each array for easy compacting
Public ahNotify(0 To cLastNotify + 1) As Long
Public aconNotify(0 To cLastNotify + 1) As TConnection
Public aerr(errFirst To errLast) As String
' Count of connected objects managed by class
Public cObject As Long

I use arrays with a fixed size because it’s easier than dealing with dynamic structures. In a single-threaded server, you’d have one handle and one connection block for each client, but the Notify server is marked for unattended execution with a thread pool of four. Each of those threads will have its own array of handles, so potentially you could have more than 100 clients. Realistically it’s pretty unlikely that the server would have more than four simultaneous clients. Regardless of threads and clients, the notification handles must be arranged in an array because, as you’ll see shortly, that’s the way WaitForMultipleHandles expects them. That’s why handles and object data are kept in parallel arrays. Notice that the array is public data residing in NOTIFY.BAS, not in NOTIFY.CLS. Every client (within the same thread) shares the same arrays of handles and notifications.


Here’s how the Connect method initializes this data for each notification request:

Function Connect(notifier As IFileNotifier, sDir As String, _
efn As EFILE_NOTIFY, fSubTree As Boolean) As Long
Connect = hInvalid ' Assume fail
Dim i As Long, h As Long
' Find blank handle space
For i = 0 To cLastNotify
If ahNotify(i) = hInvalid Then
' Set up notification
h = FindFirstChangeNotification(sDir, fSubTree, efn)
Connect = h
If h = hInvalid Then
' Change notification unsupported on remote disks
If Err.LastDllError <> ERROR_NOT_SUPPORTED Then
RaiseError errInvalidArgument
End If
Exit Function
End If
' Store information
ahNotify(i) = h
With aconNotify(i)
Set .notifier = notifier
.sDir = sDir
.efn = efn
.fSubTree = fSubTree
End With
Exit Function
End If
Next
RaiseError errTooManyNotifications
End Function

Connect looks for a blank slot in the array. When it finds one, it initializes the slot with a file notification handle obtained by calling the FindFirst­­-
ChangeNotification API function. You can see what happens to the parameters passed by the client. If everything goes well, Connect stores the handle and other data in the appropriate arrays. You can look through the code to see the details of how RaiseError handles errors and Disconnect undoes the work done by Connect.


The important point about this code is that FindFirstChangeNotification requests that the kernel send off a thread to watch for file events. All you need to do is wait for the event to happen. But wait where? Who will the kernel notify when it gets a file event? It can’t wait in the Connect event (which must return to the client), but where else can it go?


The only other place is in the server’s standard module. You need a loop that checks periodically for changes. Theoretically, you could put the loop in Sub Main (as I did in the previous version of this book). But changes in the way Visual Basic handles server startup forced me to change. I use the Main procedure to initialize server data, but at some point I have to return control to the client. So instead of looping in Main, I use a Windows timer to start a separate loop procedure. Here’s the initialization code:

Sub Main()

Dim i As Integer
For i = 0 To cLastNotify
ahNotify(i) = hInvalid
Next
aerr(errInvalidDirectory) = "Invalid directory"
aerr(errInvalidType) = "Invalid notification type"
aerr(errInvalidArgument) = "Invalid argument"
aerr(errTooManyNotifications) = "Too many notifications"
aerr(errNotificationNotFound) = "Notification not found"
BugMessage "Initialized static data"

' Start the wait loop and return to the caller
Call SetTimer(hNull, 0, 200, AddressOf WaitForNotify)
BugMessage "Started Timer"

End Sub

The SetTimer function specifies that a callback called WaitForNotify will be started every 200 milliseconds. You can see a better example of a Windows timer in the CTimer class (TIMER.BAS and TIMER.CLS), which I mentioned in Chapter 7. The callback procedure must satisfy the very specific format shown below. The first part of this code simply kills the timer (we only want it to execute once). After that, it starts the real notification loop:

Sub WaitForNotify(ByVal hWnd As Long, ByVal iMsg As Long, _
ByVal idTimer As Long, ByVal cCount As Long)
' Ignore all parameters except idTimer

' This one-time callback is used only to start the loop
KillTimer hNull, idTimer
BugMessage "Killed Timer"

Dim iStatus As Long, f As Boolean
' Keep waiting for file change events until no more objects
Do
' Wait 100 milliseconds for notification
iStatus = WaitForMultipleObjects(Count, ahNotify(0), _
False, 100)
Select Case iStatus
Case WAIT_TIMEOUT
' Nothing happened
BugMessage "Waited for timeout"
DoEvents
Case 0 To Count
BugMessage "Got a notification"
' Ignore errors from client; that's their problem
On Error Resume Next
' Call client object with information
With aconNotify(iStatus)
.notifier.Change .sDir, .efn, .fSubTree
End With
' Wait for next notification
f = FindNextChangeNotification(ahNotify(iStatus))
BugAssert f
Case WAIT_FAILED
' Indicates no notification requests
' BugMessage "No notification requests"
DoEvents
Case Else
BugMessage "Can't happen"
End Select
' Class Initialize and Terminate events keep reference count
Loop Until cObject = -1
End Sub

The WaitForMultipleObjects function waits for what Windows calls an object. A Win32 object can be a process, a thread, a mutex, an event, a semaphore, console input, or a change notification. I’m not even going to define these things, much less explain why or how you would want to wait on one. The point is that you must put the handles of those objects in a contiguous array and pass the number of objects, followed by the address of the first object. You must also indicate whether you want to wait until all objects have returned or wait only for the first one. In this case, you pass False to wait for the first file notification object. Finally you pass the timeout period, 100 milliseconds.


When I say that WaitForMultipleObjects waits, I mean that literally. As soon as the thread executing this Visual Basic server code hits WaitForMultipleObjects, it stops dead. The server is no longer running. All the other programs in the system get all the cycles, and you get nothing. That’s what you asked for. If you doubt it, change the timeout period to the constant INFINITE (–1). The server locks tight, and absolutely nothing happens in it except responses to file events. The client keeps running and responding to file change notifications, but when the client tries to call the server’s Disconnect method, no one’s home. A timeout period is desperately needed in the WAIT_TIMEOUT case so that the server can get control and respond to other Connect and Disconnect requests.


When a file notification object does come through, WaitForMultipleObjects returns its index into the handle array. The Case 0 to Count block handles the notifications by using the stored client object to call the client’s FileChange event procedure. It must then call FindNextChangeNotification to wait for the next event. Incidentally, Count is simply a Property Get procedure that counts the handles in the array.