October 1999
Keeping an Eye on Your NTFS Drives, Part II: Building a Change Journal Application |
The Change Journal can be either active or disabled on an NTFS volume. If the Change Journal is active, the DeviceIoControl function will return TRUE. Any app that wants to use the Change Journal can activate it if it is disabled. The system can perform this operation very quickly. |
This article assumes you're familiar with Windows NT, Platform SDK |
Code for this article: ChangeJournal2.exe (22KB)
Jeffrey Cooperstein is an author, trainer, and Windows programming consultant. He can be reached at www.cooperstein.com. Jeffrey Richter wrote Advanced Windows, Third Edition (Microsoft Press, 1997) and Windows 95: A Developer's Guide (M&T Books, 1995). Jeff is a consultant and teaches Win32 programming courses (www.solsem.com). He can be reached at www.JeffreyRichter.com. |
Last month (September 1999) we introduced you to the new Change Journal functionality of Windows® 2000. We explained its implementation, and provided enough information to dump the contents of the Change Journal on an NTFS 5.0 volume. This month we'll cover the rest of what you need to know to write a fully functional Change Journal application. The sample application, CJTest, will demonstrate all the techniques described in this article, and can be used as a template for your own application. Change Journal States
|
|
The MaximumSize member is the maximum number of bytes the journal should use on the volume. It should be some small percentage of the size of the volume, and is limited to 4GB. For example, a reasonable size for a 9GB drive is an 8MB Change Journal. The AllocationDelta member specifies the number of bytes the journal file should expand when needed. This should be an even multiple of the volume's cluster size, and approximately one-eighth to one-quarter the value of MaximumSize. The AllocationDelta is also the number of bytes that will be purged from the start of the file if the file grows past MaximumSize. The system is lazy about enforcing the MaximumSize of a journal. It is possible that the journal can temporarily grow past this limit, but eventually the system will purge the old records. If a Change Journal already exists on a volume, calls to FSCTL_CREATE_USN_JOURNAL will still succeed, and it will update the journal's MaximumSize and AllocationDelta parameters. This allows you to expand the number of records that an active journal maintains without having to disable it. Any application that wants to use the Change Journal can activate it if it is disabled. Fortunately, the system can perform this operation very quickly. On the other hand, disabling (or deleting) an active journal can be quite time-consuming because the system must walk all the records in the master file table (MFT) and set the Last USN attribute to zero. This process can take several minutes, and will continue across reboots of the system if necessary. During this process, the journal is not considered active, but it is also not disabled (see Figure 1). |
Figure 1: Possible Change Journal States |
While the system is disabling the journal, it cannot be accessed and all journal operations return ERROR_ JOURNAL_DELETE_IN_PROGRESS. Applications should never disable an active journal because it will adversely affect other applications using the journal. However, if you really want to disable a journal you can do it by calling DeviceIoControl, passing the FSCTL_ DELETE_USN_JOURNAL code to it. The following structure must also be supplied: |
|
UsnJournalID should be zero unless the USN_DELETE_FLAG_DELETE flag is specified in the DeleteFlags memberin this particular case, it must be set to the ID of the active journal.
The DeleteFlags member can be either USN_DELETE_ FLAG_DELETE or USN_DELETE_FLAG_NOTIFY. If the USN_DELETE_FLAG_DELETE flag is specified alone, the system will disable the active journal. DeviceIoControl will return immediatelyit does not wait for the journal to be disabled. The journal must be active and UsnJournalID must be set to its ID or the call will fail. If the USN_ DELETE_FLAG_NOTIFY flag is specified in addition to USN_DELETE_FLAG_DELETE, DeviceIoControl returns only after the journal is fully disabled. The USN_DELETE_FLAG_NOTIFY flag can be specified alone to wait for the journal to become available after receiving ERROR_JOURNAL_DELETE_IN_PROGRESS from any of the Change Journal functions. The following code fragment illustrates how you can wait for a journal to be disabled: |
|
Be aware that if the USN_
DELETE_FLAG_NOTIFY flag is used with asynchronous I/O, DeviceIoControl might return while the journal is still disabling. In this case, DeviceIoControl returns FALSE, GetLastError returns ERROR_IO_PENDING, and any journal operations will return ERROR_JOURNAL_DELETE_IN_PROGRESS. You can wait on the device handle or the OVERLAPPED structure's event handle to become signaled, indicating that the journal is now available.
The fact that applications cannot gain exclusive access to the Change Journal has some interesting repercussions. Consider the following pseudocode, which disables the journal and waits for it to become available:
There are two mistakes in this code. First, QueryUsnJournal could return ERROR_JOURNAL_DELETE_
IN_PROGRESS. In this case, DisableUsnJournal must be called using only the USN_DELETE_FLAG_NOTIFY flag in order to wait for the journal to be available. The second mistake is that the DisableUsnJournal call itself may return ERROR_JOURNAL_DELETE_IN_PROGRESS. Imagine the case where QueryUsnJournal successfully returns the active journal's ID, but another application immediately starts to disable it. The call to DisableUsnJournal will return ERROR_
JOURNAL_DELETE_IN_
PROGRESS, and the journal will not be available. The following pseudocode will be more successful:
|
|
As you can see, a simple operation like disabling the journal is still not straightforward. Even the pseudocode is difficult to read. Consider everything an application has to do when it startsopen the volume handle, wait if the journal is currently being deleted, activate the journal if it is disabled, and, finally, query for the journal's information. Even worse, at any step of the way, another application can be trying to activate or disable the journal. To simplify things, it helps to create a single function that doesn't return until the journal is active.
The flow chart in Figure 2 illustrates a robust process that will fill in a USN_JOURNAL_DATA structure. It is robust since it will do everything possible to ensure that the journal is active. |
Figure 2: Filling a USN_JOURNAL_DATA Structure |
New Record Notification
Last month we showed how to dump existing records in the Change Journal. After processing all available records, an application will want to wait for new records to be available. A simple solution would be to poll the journal's NextUsn parameter using the FSCTL_QUERY_USN_ JOURNAL code. Fortunately, there are more elegant solutions that don't require the polling technique. The following code shows how to wait for a specified USN to exist in the journal. It does not return the record. It is assumed that the specified USN does not currently exist in the journal, but the function will just return immediately if it does exist. |
|
An application can obtain the USN of the next record that will be written to the journal by looking at the NextUsn value returned with the FSCTL_QUERY_USN_JOURNAL code. If this is used with the previous code, the function will return as soon as a new record is written to the journal.
Another way an application can use the WaitForNextUsn function is after it has processed all existing records in the journal. In last month's code, we walked all available records using repetitive calls to DeviceIoControl with the FSCTL_ READ_USN_JOURNAL code. We assumed that all records were processed when DeviceIoControl returned only sizeof(USN) bytes in the output buffer. When this happens, the first sizeof(USN) bytes of the output buffer will actually contain the same value as the StartUsn member specified in the input buffer. This means that you've read all available records and the USN returned will be the USN of the next record that will be created. The application can wait for more data to become available by using this USN in a call to the WaitForNextUsn function defined earlier. If an application uses the WaitForNextUsn function, it should probably sleep for a short period of time before processing the new records. If another application is performing multiple disk operations, a short delay will allow several journal records to be created before they are processed. Otherwise, the Change Journal application will be competing for system resources with the application making the changes. In addition, the Change Journal application may find that it is processing many small chunks of records as another application is doing its work. Another way to avoid this problem is to specify BytesToWaitFor or Timeout values. This allows an application to process journal entries only after a large number of new records are generated (or a specified time has passed). If you used the following values in the function defined earlier, |
|
the function would not return immediately when the specified USN is created. Instead, as soon as the USN is created, it will wait until an additional 16KB of raw journal data is availableor every 25 seconds it checks to see if any records exist past the specified USN, then will return. The BytesToWaitFor and Timeout members are particularly useful if you specify filter conditions with the ReasonMask and ReturnOnlyOnClose members. Instead of processing all new records as they appear in the journal, the system will wait until it has at least one record that passes the filter conditions before returning. Examining the MFT for Last USN
|
|
The LowUsn and HighUsn members are the USNs used to limit the search. The first time this function is called, the StartFileReferenceNumber member should be set to zero. The output buffer will be filled with as much information as it can hold. The MFT is enumerated from lowest FRN to highest FRN. If more data is available, the first eight bytes of the output buffer will be the FRN to use in the next call to DeviceIoControl with the FSCTL_ENUM_USN_DATA code. If you've enumerated all the data, this FRN will be higher than the highest FRN in use by the MFT; the next call using the FSCTL_ENUM_USN_DATA code will then return FALSE, and GetLastError will return ERROR_
HANDLE_EOF.
Following the first eight bytes of the output buffer is an array of USN_RECORD structures. The FileReferenceNumber and Usn members specify the Last USN of the file. Although the output buffer is very similar to FSCTL_READ_ USN_JOURNAL, it is not returning records from the journal. Instead, it is just using the USN_RECORD structure as a convenient way to return the Last USN data. Figure 3 illustrates how you can list all files that do not have valid journal data. In this case, it will be listing all files with a Last USN below the FirstUsn that is available in the journal. The FSCTL_ENUM_USN_DATA code is intended to give applications an easy way to recover from situations where journal data is purged before it can be processed. Imagine the series of events shown in Figure 4. Your application shuts down after it has processed USN 512. A call using the FSCTL_ QUERY_USN_JOURNAL code will show that the NextUsn member contains 640. When your application is restarted, it queries the journal again, but finds out that the FirstUsn in the journal is 1152. Since records from 640 up to, but not including, 1152 were lost, your application might have to throw out all cached information. For some applications, the information you get from the FSCTL_ENUM_USN_ DATA code is enough to reconstruct missing events. Specifying a LowUsn of 640 and a HighUsn of 1152 will return the following information. |
USN |
File |
Last USN |
128 |
File2 |
768 |
256 |
File5 |
1024 |
This will let the application know that something happened to File2, and you'll discover the newly created File5. Unfortunately, you won't know that File1 was deleted (it's not in the MFT any more, so there's no Last USN), or that File3 changed (you'll see that File3 changed at USN 1152, but you won't know that it changed twice while you were shut down). Depending on the type of application, this may be enough information, or it may help in deciding which files or directories need to be reexamined.
The FSCTL_READ_FILE_USN_DATA code can be used to get Change Journal-related information about a specific file or directory. The DeviceIoControl function is called with the handle to an open file or directory (not a volume handle as is the case with other Change Journal functions), and the output buffer will be filled with a single USN_ RECORD structure: |
|
|
If the call succeeds, the following members of the returned USN_RECORD structure will be valid: RecordLength, MajorVersion, MinorVersion, FileReferenceNumber, ParentFileReferenceNumber, Usn, SecurityId, FileNameOffset, and FileNameLength. The TimeStamp, Reason, and SourceInfo members will not contain valid information. The Usn member represents the Last USN written to the journal for this file or directory. The actual last record can be read (unless it has already been purged) by specifying the Usn member as the StartUsn in a subsequent call using the FSCTL_READ_
USN_JOURNAL code. File Name from File Reference Number
Maintaining the Directory Database
Supplying Source Information
|
|
The UsnSourceInfo member uses the same constants defined for the SourceInfo member of USN_RECORD. The VolumeHandle member is created the same way as for Change Journal operations. By requiring a volume handle, only applications with administrative privileges can add source information to Change Journal records. The HandleInfo member is currently reserved, and must be zero. Figure 8 illustrates the way a service should use this functionality to add private information in a named stream.
It is perfectly OK to use the FSCTL_MARK_HANDLE code even if the journal is disabled. The source information is actually associated internally with the file, not the Change Journal, so the information persists when the journal is disabled. If you want to store data in a private stream, it is a good idea to generate your private stream name from a GUID. This prevents accidentally using a name that someone else is using. A good example of a private stream is the new thumbnail view in Explorer (see Figure 9). Instead or calculating thumbnails every time you open a folder, it stores them in a private stream on each file. Microsoft generated a GUID that Explorer uses as the stream name, so it will never conflict with other applications that access the file. |
Figure 9: Windows Explorer Thumbnail View |
Wrap-up
The sample application, CJTest (see Figure 10), monitors the Change Journal and dumps information about records to the screen as they are created. It also lets you dump the current Change Journal statistics or delete the Change Journal on the current volume. Figure 11 shows CJTest as it would display a dump of the current journal information, a file being moved from C:\Directory1 to C:\Directory2, and the directory C:\Directory1 being deleted. |
Figure 11: CJTest in Action |
CJTest uses just about every technique described in our two articles. First, it ensures that the Change Journal is always active. It populates a directory database, maintains it with the Change Journal, and persists it to disk between sessions. CJTest monitors the journal for changes, and displays new records as they arrive. CJTest also gracefully recovers from situations where the journal ID changes or the journal is disabled by another application. To test this, a Delete Journal button is provided. CJTest will automatically activate the journal as soon as possible. (It uses FSCTL_DELETE_USN_JOURNAL to be notified when the journal is available if it ever receives ERROR_JOURNAL_DELETE_IN_PROGRESS.) When CJTest starts, it uses the Change Journal, if possible, to bring its cached directory database up to date. Since each volume may have its own Change Journal, CJTest just uses the current drive letter when picking a volume to examine. Before you begin writing your own code, play around with CJTest and see how the system behaves. You should now have the tools to create a useful Change Journal app. |
For related information see: NTFS Change Journal at: http://msdn.microsoft.com/library/psdk/winbase/fsys_3xrg.htm. Also check http://msdn.microsoft.com for daily updates on developer programs, resources and events. |
From the October 1999 issue of Microsoft Systems Journal.
|