FileSystemWatcher Fencing(Part 2)


 

This post is a follow up to the FileSystemWatcher Follies post.  I received a lot of feedback that it would be useful to highlight what would be appropriate to guide against some of the pitfalls that I mentioned in that post.  I’ll cover several of the issues here over a couple of posts and propose things that could be done to detect that they are there before using the FileSystemWatcher class against them.  Though the code examples will all be in C#, there will be some P/Invoke involved here as not all of this functionality is exposed through .NET Framework classes at this time.

Using Change Journals

If you’ve already determined that your path is local and uses the NTFS or ReFS file system, a great alternative to the FileSystemWatcher is to use change journaling.  Change journals can be complicated, but they also give you very fine grained control over the information that you want.  However, your code must be running as an administrator or system in order to create or delete them, and they do take up some space on disk (the maximum amount that will be taken up can be specified).    Because change journals monitor an entire volume, if you’re designing an application to make optimal use of this functionality for consistent change monitoring, you may want to put the data that you’re consistently monitoring for changes on its own volume.

Other things to keep in mind when using change journals:

  1. Changes for files and directories are not full paths; parent directories are identified by IDs and those directory names can be looked up by OpenFileByID amongst other methods
  2. If BytesToRead is set to zero, it will immediately return with up to one entry; otherwise it will wait until that many bytes are filled in to the buffer or the specified timeout value.  If you want to get immediate notification
  3. It does not work on network file paths.
  4. All of the functionality works through the use of DeviceIOControl; consult the documentation for the structure type and enumeration value for additional details about how to use that value.

Basic Change Journal Wrapper

Below is sample code for a basic change journal class which monitors a volume for changes.  The changes monitored for are specifiable and the types available are included as an enumeration.  The values are hardcoded to only show file creation and delete events.  If the buffer size is set to anything less than 1024, the sample will use zero for BytesToRead to immediately return upon receiving each entry.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.IO;
using System.Threading;

namespace ChangeJournal
{
    public class ChangeJournalHandle : SafeHandleMinusOneIsInvalid
    {

        [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern IntPtr CreateFileW(
            [MarshalAs(UnmanagedType.LPWStr)]
            string FileName,
            int DesiredAccess,
            FileShare ShareMode,
            IntPtr SecurityAttributes,
            int CreationDisposition,
            int FlagsAndAttributes,
            IntPtr hTemplateFile
            );

        [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern int GetVolumeInformationByHandleW(
  IntPtr hFile,
  StringBuilder lpVolumeNameBuffer,
  int nVolumeNameSize,
  out int lpVolumeSerialNumber,
  out int
     lpMaximumComponentLength,
  out int lpFileSystemFlags,
  StringBuilder lpFileSystemNameBuffer,
  int nFileSystemNameSize
);



        [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern int DeviceIoControl(
    IntPtr hDevice,
    int dwIoControlCode,
    IntPtr lpInBuffer,
    int nInBufferSize,
    IntPtr lpOutBuffer,
    int nOutBufferSize,
    out int lpBytesReturned,
    IntPtr lpOverlapped
    );

        [DllImport("kernel32", SetLastError = true)]
        private static extern bool CloseHandle(
            IntPtr handle);


        [DllImport("kernel32", SetLastError = true)]
        private static extern IntPtr OpenFileById(
  IntPtr hFile,
  ref FILE_ID_DESCRIPTOR lpFileID,
  int                 dwDesiredAccess,
  FileShare dwShareMode,
  IntPtr lpSecurityAttributes,
  int dwFlags
);
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern int GetFinalPathNameByHandleW(
  IntPtr hFile,
  StringBuilder lpszFilePath,
  int cchFilePath,
  int dwFlags
);
        [StructLayout(LayoutKind.Explicit)]
        public struct FILE_ID_DESCRIPTOR
        { 
            [FieldOffset(0)]
            public int Size;
            [FieldOffset(4)]
            public int Type;
            [FieldOffset(8)]
            public long FileId;
            [FieldOffset(8)]
            public Guid ObjectId;
            [FieldOffset(8)]
            public Guid ExtendedFileId; //Use for ReFS; need to use v3 structures or later instead of v2 as done in this sample
        }

        public static int CTL_CODE(int DeviceType, int Function, int Method, int Access)
        {
            return ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method);
        }

        protected override bool ReleaseHandle()
        {

            if(handle != IntPtr.Zero)
            {
                if(createdJournal == true)
                {
                    TryDeleteCurrentJournal();
                }
                StopListening(10);//this may cause a delay
                return CloseHandle(handle);
            }
            return false;
        }

        public const int FILE_DEVICE_FILE_SYSTEM = 0x00000009;
        public const int METHOD_BUFFERED = 0;
        public const int METHOD_IN_DIRECT = 1;
        public const int METHOD_OUT_DIRECT = 2;
        public const int METHOD_NEITHER = 3;
        public const int FILE_ANY_ACCESS = 0;

        public static int FSCTL_READ_USN_JOURNAL = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 46, METHOD_NEITHER, FILE_ANY_ACCESS);
        public static int FSCTL_ENUM_USN_DATA = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 44, METHOD_NEITHER, FILE_ANY_ACCESS);
        public static int FSCTL_CREATE_USN_JOURNAL = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 57, METHOD_NEITHER, FILE_ANY_ACCESS);
        public static int FSCTL_READ_FILE_USN_DATA = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 58, METHOD_NEITHER, FILE_ANY_ACCESS);
        public static int FSCTL_QUERY_USN_JOURNAL = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 61, METHOD_BUFFERED, FILE_ANY_ACCESS);
        public static int FSCTL_DELETE_USN_JOURNAL = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 62, METHOD_BUFFERED, FILE_ANY_ACCESS);
        public static int FSCTL_WRITE_USN_REASON = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 180, METHOD_BUFFERED, FILE_ANY_ACCESS);
        public static int FSCTL_USN_TRACK_MODIFIED_RANGES = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 189, METHOD_BUFFERED, FILE_ANY_ACCESS);

        [StructLayout(LayoutKind.Sequential)]
        public struct USN
        {
            public long Usn;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct MFT_ENUM_DATA_V0
        {
            public USN Low;
            public USN High;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct MFT_ENUM_DATA_V1
        {
            public long StartFileReferenceNumber;
            public USN Low;
            public USN High;
            public short MinMajorVersion;
            public short MaxMajorVersion;
        }


        [StructLayout(LayoutKind.Sequential)]
        public struct CREATE_USN_JOURNAL_DATA
        {
            public long MaximumSize;
            public long AllocationDelta;
        }


        [StructLayout(LayoutKind.Sequential)]
        public struct READ_USN_JOURNAL_DATA_V0
        {
            public USN StartUsn;
            public int ReasonMask;
            public int ReturnOnlyOnClose;
            public long Timeout;
            public long BytesToWaitFor;
            public long UsnJournalId;
        }


        [StructLayout(LayoutKind.Sequential)]
        public struct READ_USN_JOURNAL_DATA_V1
        {
            public USN StartUsn;
            public int ReasonMask;
            public int ReturnOnlyOnClose;
            public long Timeout;
            public long BytesToWaitFor;
            public long UsnJournalId;
            public short MinMajorVersion;
            public short MaxMajorVersion;
        }


        [StructLayout(LayoutKind.Sequential)]
        public struct USN_TRACK_MODIFIED_RANGES
        {
            public int Flags;
            public int Unused;
            public long ChunkSize;
            public long FileSizeThreshold;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_RANGE_TRACK_OUTPUT
        {
            public USN Usn;
        }

        public const int FLAG_USN_TRACK_MODIFIED_RANGES_ENABLE = 0x00000001;

        public class UsnRecordV2WithName
        {
            public USN_RECORD_V2 Record { get; set; }
            public string Filename { get; set; }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_RECORD_V2
        {
            public int RecordLength;
            public short MajorVersion;
            public short MinorVersion;
            public long FileReferenceNumber;
            public long ParentFileReferenceNumber;
            USN Usn;
            public long TimeStamp;
            public int Reason;
            public int SourceInfo;
            public int SecurityId;
            public int FileAttributes;
            public short FileNameLength;
            public short FileNameOffset;
            //WCHAR FileName[1];

        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_RECORD_V3
        {
            public int RecordLength;
            public short MajorVersion;
            public short MinorVersion;
            public Guid FileReferenceNumber;
            public Guid ParentFileReferenceNumber;
            USN Usn;
            public long TimeStamp;
            public int Reason;
            public int SourceInfo;
            public int SecurityId;
            public int FileAttributes;
            public short FileNameLength;
            public short FileNameOffset;
            //WCHAR FileName[1];

        }


        [StructLayout(LayoutKind.Sequential)]
        public struct USN_RECORD_COMMON_HEADER
        {
            public int RecordLength;
            public short MajorVersion;
            public short MinorVersion;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_RECORD_EXTENT
        {
            public long Offset;
            public long Length;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_RECORD_V4
        {
            public USN_RECORD_COMMON_HEADER Header;
            public Guid FileReferenceNumber;
            public Guid ParentFileReferenceNumber;
            public USN Usn;
            public int Reason;
            public int SourceInfo;
            public int RemainingExtents;
            public short NumberOfExtents;
            public short ExtentSize;
            public USN_RECORD_EXTENT Extents; //Extents[1]
        }


        public const int USN_PAGE_SIZE = (0x1000);
        public const int USN_REASON_DATA_OVERWRITE = (0x00000001);
        public const int USN_REASON_DATA_EXTEND = (0x00000002);
        public const int USN_REASON_DATA_TRUNCATION = (0x00000004);
        public const int USN_REASON_NAMED_DATA_OVERWRITE = (0x00000010);
        public const int USN_REASON_NAMED_DATA_EXTEND = (0x00000020);
        public const int USN_REASON_NAMED_DATA_TRUNCATION = (0x00000040);
        public const int USN_REASON_FILE_CREATE = (0x00000100);
        public const int USN_REASON_FILE_DELETE = (0x00000200);
        public const int USN_REASON_EA_CHANGE = (0x00000400);
        public const int USN_REASON_SECURITY_CHANGE = (0x00000800);
        public const int USN_REASON_RENAME_OLD_NAME = (0x00001000);
        public const int USN_REASON_RENAME_NEW_NAME = (0x00002000);
        public const int USN_REASON_INDEXABLE_CHANGE = (0x00004000);
        public const int USN_REASON_BASIC_INFO_CHANGE = (0x00008000);
        public const int USN_REASON_HARD_LINK_CHANGE = (0x00010000);
        public const int USN_REASON_COMPRESSION_CHANGE = (0x00020000);
        public const int USN_REASON_ENCRYPTION_CHANGE = (0x00040000);
        public const int USN_REASON_OBJECT_ID_CHANGE = (0x00080000);
        public const int USN_REASON_REPARSE_POINT_CHANGE = (0x00100000);
        public const int USN_REASON_STREAM_CHANGE = (0x00200000);
        public const int USN_REASON_TRANSACTED_CHANGE = (0x00400000);
        public const int USN_REASON_INTEGRITY_CHANGE = (0x00800000);
        public const uint USN_REASON_CLOSE = (0x80000000);

        [Flags]
        public enum UsnReasonType : int
        {
            USN_REASON_DATA_OVERWRITE = (0x00000001),
            USN_REASON_DATA_EXTEND = (0x00000002),
            USN_REASON_DATA_TRUNCATION = (0x00000004),
            USN_REASON_NAMED_DATA_OVERWRITE = (0x00000010),
            USN_REASON_NAMED_DATA_EXTEND = (0x00000020),
            USN_REASON_NAMED_DATA_TRUNCATION = (0x00000040),
            USN_REASON_FILE_CREATE = (0x00000100),
            USN_REASON_FILE_DELETE = (0x00000200),
            USN_REASON_EA_CHANGE = (0x00000400),
            USN_REASON_SECURITY_CHANGE = (0x00000800),
            USN_REASON_RENAME_OLD_NAME = (0x00001000),
            USN_REASON_RENAME_NEW_NAME = (0x00002000),
            USN_REASON_INDEXABLE_CHANGE = (0x00004000),
            USN_REASON_BASIC_INFO_CHANGE = (0x00008000),
            USN_REASON_HARD_LINK_CHANGE = (0x00010000),
            USN_REASON_COMPRESSION_CHANGE = (0x00020000),
            USN_REASON_ENCRYPTION_CHANGE = (0x00040000),
            USN_REASON_OBJECT_ID_CHANGE = (0x00080000),
            USN_REASON_REPARSE_POINT_CHANGE = (0x00100000),
            USN_REASON_STREAM_CHANGE = (0x00200000),
            USN_REASON_TRANSACTED_CHANGE = (0x00400000),
            USN_REASON_INTEGRITY_CHANGE = (0x00800000),
            USN_REASON_CLOSE = unchecked((int)(0x80000000))
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_JOURNAL_DATA_V0
        {
            public long UsnJournalID;
            public USN FirstUsn;
            public USN NextUsn;
            public USN LowestValidUsn;
            public USN MaxUsn;
            public long MaximumSize;
            public long AllocationDelta;

        }
        [StructLayout(LayoutKind.Sequential)]
        public struct USN_JOURNAL_DATA_V1
        {
            public long UsnJournalID;
            public USN FirstUsn;
            public USN NextUsn;
            public USN LowestValidUsn;
            public USN MaxUsn;
            public long MaximumSize;
            public long AllocationDelta;
            public short MinSupportedMajorVersion;
            public short MaxSupportedMajorVersion;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct USN_JOURNAL_DATA_V2
        {
            public long UsnJournalID;
            public USN FirstUsn;
            public USN NextUsn;
            public USN LowestValidUsn;
            public USN MaxUsn;
            public long MaximumSize;
            public long AllocationDelta;
            public short MinSupportedMajorVersion;
            public short MaxSupportedMajorVersion;
            public int Flags;
            public long RangeTrackChunkSize;
            public long RangeTrackFileSizeThreshold;
        }



        [StructLayout(LayoutKind.Sequential)]
        public struct DELETE_USN_JOURNAL_DATA
        {
            public long UsnJournalID;
            public int DeleteFlags;
        }

        public int USN_DELETE_FLAG_DELETE = (0x00000001);
        public int USN_DELETE_FLAG_NOTIFY = (0x00000002);
        public int USN_DELETE_VALID_FLAGS = (0x00000003);

        public UsnReasonType EventTriggerMask
        {
            get
            {
                return (UsnReasonType)rdata.ReasonMask;
            }
            set
            {
                rdata.ReasonMask = (int)value;
            }
        }

        public long Timeout
        {
            get
            {
                return rdata.Timeout;
            }
            set
            {
                rdata.Timeout = value;
            }
        }

        public bool TriggerOnCloseOnly
        {
            get
            {
                return rdata.ReturnOnlyOnClose != 0;
            }
            set
            {
                rdata.ReturnOnlyOnClose = value ? 1 : 0;
            }
        }
        private ReaderWriterLockSlim readBufferLock = new ReaderWriterLockSlim();

        private int readBufferSize = 8192;

        //This could hang if there is a long timeout value
        public int ReadBufferSize
        {
            get
            {
                readBufferLock.EnterWriteLock();
                try
                {
                    return readBufferSize;
                }
                finally
                {
                    readBufferLock.ExitWriteLock();
                }
            }
            set
            {
                readBufferLock.EnterWriteLock();
                try
                {

                    if (value > 0)
                    {
                        if (readBuffer == IntPtr.Zero)
                        {
                            readBuffer = Marshal.AllocHGlobal(value);
                        }
                        else
                        {
                            readBuffer = Marshal.ReAllocHGlobal(readBuffer, (IntPtr)value);
                        }
                    }
                    readBufferSize = value;

                }
                finally
                {
                    readBufferLock.ExitWriteLock();
                }
            }
        }
        public event Action<ChangeJournalHandle, UsnRecordV2WithName> OnChange;
        public event Action<ChangeJournalHandle, Exception> OnError;

        private bool shouldRun = false;

        private Thread thread = null;

        public ChangeJournalHandle(string path) : base(true)
        {
            //TODO:Handle taking non-volume paths
            handle = CreateFileW(path, unchecked((int)(0x80000000 | 0x40000000)),
                FileShare.ReadWrite, IntPtr.Zero, 3, 0, IntPtr.Zero);
            if (IsInvalid)
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        public bool TryCreateJournal(long maxSize = (1024 ^ 2) * 500, long allocationDelta = 8192)
        {
            CREATE_USN_JOURNAL_DATA data = new CREATE_USN_JOURNAL_DATA();
            data.AllocationDelta = allocationDelta;
            data.MaximumSize = maxSize;
            int size = Marshal.SizeOf(data);
            IntPtr buffer = Marshal.AllocHGlobal(size);
            try
            {
                int bufSizeOut;
                int result = DeviceIoControl(handle, FSCTL_CREATE_USN_JOURNAL, buffer, size, IntPtr.Zero, 0, out bufSizeOut, IntPtr.Zero);
                if (result == 0)
                {
                    ReportLastError();
                    return false;
                }
                createdJournal = true;
                return true;
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }
        public void CreateJournal(long maxSize = (1024 ^ 2) * 500, long allocationDelta = 8192)
        {
            CREATE_USN_JOURNAL_DATA data = new CREATE_USN_JOURNAL_DATA();
            data.AllocationDelta = allocationDelta;
            data.MaximumSize = maxSize;
            int size = Marshal.SizeOf(data);
            IntPtr buffer = Marshal.AllocHGlobal(size);
            try
            {
                int bufSizeOut;
                int result = DeviceIoControl(handle, FSCTL_CREATE_USN_JOURNAL, buffer, size, IntPtr.Zero, 0, out bufSizeOut, IntPtr.Zero);
                if (result == 0)
                {
                    Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
                }
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }
        public bool TryDeleteCurrentJournal()
        {
            USN_JOURNAL_DATA_V0 data = new USN_JOURNAL_DATA_V0();
            int size = Marshal.SizeOf(data);
            IntPtr buffer = Marshal.AllocHGlobal(size);
            try
            {
                int outSize;
                int result = DeviceIoControl(handle, FSCTL_QUERY_USN_JOURNAL, IntPtr.Zero, 0, buffer, size, out outSize, IntPtr.Zero);
                if (result == 0)
                {
                    ReportLastError();
                    return false;
                }
                data = Marshal.PtrToStructure<USN_JOURNAL_DATA_V0>(buffer);
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
            DELETE_USN_JOURNAL_DATA d = new DELETE_USN_JOURNAL_DATA();
            d.UsnJournalID = data.UsnJournalID;
            d.DeleteFlags = 3;
            size = Marshal.SizeOf(d);
            buffer = Marshal.AllocHGlobal(size);
            try
            {
                Marshal.StructureToPtr(d, buffer, false);
                if (DeviceIoControl(handle, FSCTL_DELETE_USN_JOURNAL, buffer, size, IntPtr.Zero, 0, out size, IntPtr.Zero) == 0)
                {
                    ReportLastError();
                    return false;
                }
                return true;
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }
        public bool TryDeleteJournal(long UsnJournalID)
        {
            //Note that overloads would be needed for different versions of the structure
            DELETE_USN_JOURNAL_DATA d = new DELETE_USN_JOURNAL_DATA();
            d.UsnJournalID = UsnJournalID;
            d.DeleteFlags = 3;
            int size = Marshal.SizeOf(d);
            IntPtr buffer = Marshal.AllocHGlobal(size);
            try
            {
                Marshal.StructureToPtr(d, buffer, false);
                if (DeviceIoControl(handle, FSCTL_DELETE_USN_JOURNAL, buffer, size, IntPtr.Zero, 0, out size, IntPtr.Zero) == 0)
                {
                    ReportLastError();
                    return false;
                }
                return true;
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }

        }
        public void DeleteAllJournals()
        {
            try
            {
                while (true)
                {
                    USN_JOURNAL_DATA_V0 data = new USN_JOURNAL_DATA_V0();
                    int size = Marshal.SizeOf(data);
                    IntPtr buffer = Marshal.AllocHGlobal(size);
                    try
                    {
                        int outSize;
                        int result = DeviceIoControl(handle, FSCTL_QUERY_USN_JOURNAL, IntPtr.Zero, 0, buffer, size, out outSize, IntPtr.Zero);
                        if (result == 0)
                        {
                            ReportLastError();
                            break;
                        }
                        data = Marshal.PtrToStructure<USN_JOURNAL_DATA_V0>(buffer);
                    }
                    finally
                    {
                        Marshal.FreeHGlobal(buffer);
                    }
                    DELETE_USN_JOURNAL_DATA d = new DELETE_USN_JOURNAL_DATA();
                    d.UsnJournalID = data.UsnJournalID;
                    d.DeleteFlags = 3;
                    size = Marshal.SizeOf(d);
                    buffer = Marshal.AllocHGlobal(size);
                    try
                    {
                        Marshal.StructureToPtr(d, buffer, false);
                        if (DeviceIoControl(handle, FSCTL_DELETE_USN_JOURNAL, buffer, size, IntPtr.Zero, 0, out size, IntPtr.Zero) == 0)
                        {
                            ReportLastError();
                            break;
                        }
                    }
                    finally
                    {
                        Marshal.FreeHGlobal(buffer);
                    }
                }
            }
            catch (Exception ex)
            {
                ReportException(ex);
            }
        }
        public void StartListening()
        {
            //See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365736(v=vs.85).aspx
            if (!shouldRun)
            {
                thread = new Thread(ListenProc);
                shouldRun = true;
                thread.Start();
            }
        }

        public void StopListening(int timeout = int.MaxValue)
        {
            if (shouldRun)
            {
                shouldRun = false;
                if (thread != null)
                {
                    if(!thread.Join(timeout))
                    {
                        thread.Abort();
                    }
                    thread = null;
                }
            }
        }

        public string GetNameForId(long id)
        {
            try
            {
                FILE_ID_DESCRIPTOR fid = new FILE_ID_DESCRIPTOR();
                fid.FileId = id;
                fid.Size = Marshal.SizeOf(fid);
                IntPtr h = OpenFileById(handle, ref fid, unchecked((int)0x80), FileShare.ReadWrite | FileShare.Delete, IntPtr.Zero, 0);
                if (h == new IntPtr(-1))
                {
                    Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
                }
                int size = 1024;
                StringBuilder sb = new StringBuilder(size);
                if (GetFinalPathNameByHandleW(h, sb, size, 0) == 0)
                {
                    int hr = Marshal.GetHRForLastWin32Error();
                    CloseHandle(h);
                    Marshal.ThrowExceptionForHR(hr);
                }
                CloseHandle(h);
                return sb.ToString();
            }
            catch(Exception ex)
            {
                ReportException(ex);
                return id.ToString("X");
            }

        }
        private READ_USN_JOURNAL_DATA_V0 rdata = new READ_USN_JOURNAL_DATA_V0() { ReasonMask = unchecked((int)0xFFFFFFFF) };
        private IntPtr readBuffer;
        private bool createdJournal = false;
        void ListenProc()
        {
            try
            {
                USN_JOURNAL_DATA_V0 data = new USN_JOURNAL_DATA_V0();
                int size = Marshal.SizeOf(data);
                IntPtr buffer = Marshal.AllocHGlobal(size);
                try
                {
                    int outSize;
                    int result = DeviceIoControl(handle, FSCTL_QUERY_USN_JOURNAL, IntPtr.Zero, 0, buffer, size, out outSize, IntPtr.Zero);
                    if (result == 0)
                    {
                        if(TryCreateJournal())
                        {
                            result = DeviceIoControl(handle, FSCTL_QUERY_USN_JOURNAL, IntPtr.Zero, 0, buffer, size, out outSize, IntPtr.Zero);
                        }
                        if(result == 0) ReportLastError();
                    }
                    if(result != 0) data = Marshal.PtrToStructure<USN_JOURNAL_DATA_V0>(buffer);
                }
                finally
                {
                    Marshal.FreeHGlobal(buffer);
                }
                rdata.UsnJournalId = data.UsnJournalID;
                rdata.StartUsn.Usn = 0;
                int rsize = Marshal.SizeOf(typeof(USN_RECORD_V2));
                size = Marshal.SizeOf(rdata);
                buffer = Marshal.AllocHGlobal(size);
                if (readBuffer == IntPtr.Zero)
                {
                    //Allocates the buffer if it's empty
                    ReadBufferSize = ReadBufferSize;
                }
                int usize = Marshal.SizeOf(typeof(USN));
                try
                {
                    List<UsnRecordV2WithName> records = new List<UsnRecordV2WithName>();
                    while (shouldRun)
                    {
                        records.Clear();
                        int outSize;
                        int result;
                        if(readBufferSize >= 1024)
                        {
                            rdata.BytesToWaitFor = readBufferSize;
                        }
                        else
                        {
                            //Returns immediately
                            rdata.BytesToWaitFor = 0;
                        }
                        Marshal.StructureToPtr(rdata, buffer, false);

                        readBufferLock.EnterReadLock();
                        try
                        {
                            result = DeviceIoControl(handle, FSCTL_READ_USN_JOURNAL, buffer, size, readBuffer, readBufferSize, out outSize, IntPtr.Zero);
                            if (result != 0 && outSize >= usize)
                            {
                                USN usn = Marshal.PtrToStructure<USN>(readBuffer);
                                rdata.StartUsn = usn;
                                int retbytes = outSize - usize;
                                IntPtr record = IntPtr.Add(readBuffer, usize);
                                while (retbytes > 0)
                                {
                                    USN_RECORD_V2 r = Marshal.PtrToStructure<USN_RECORD_V2>(record);
                                    UsnRecordV2WithName r2 = new UsnRecordV2WithName();
                                    r2.Record = r;
                                    r2.Filename = Marshal.PtrToStringUni(IntPtr.Add(record, r.FileNameOffset), (r.FileNameLength / 2));
                                    records.Add(r2);
                                    record = IntPtr.Add(record, r.RecordLength);
                                    retbytes -= r.RecordLength;
                                }
                            }
                            else
                            {
                                ReportLastError();
                            }
                        }
                        finally
                        {
                            readBufferLock.ExitReadLock();
                        }
                        foreach (var r in records)
                        {
                            ReportChange(r);
                        }
                    }
                }
                finally
                {
                    Marshal.FreeHGlobal(buffer);
                }
            }
            catch (ThreadAbortException tae)
            {

            }
        }

        void ReportChange(UsnRecordV2WithName record)
        {
            if (OnChange != null)
            {
                OnChange(this, record);
            }
        }

        void ReportLastError()
        {
            ReportException(Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
        }

        void ReportException(Exception ex)
        {
            if (OnError != null)
            {
                OnError(this, ex);
            }
        }



        private static void Cjh_OnError(ChangeJournalHandle arg1, Exception arg2)
        {
            Console.WriteLine("Error:/t{0}", arg2.ToString());
        }

        private static void Cjh_OnChange(ChangeJournalHandle arg1, UsnRecordV2WithName arg2)
        {
            //Note that it would be typically faster in the long run to build a dictionary 
            //of directory names by IDs and reset it whenever the change journal resets 
            //instead of looking up the directory each time
            //Also, note that if the directory is deleted before OpenFileById is called in GetNameById, it's going to fail with an out of range error
            Console.Write(arg1.GetNameForId(arg2.Record.ParentFileReferenceNumber));
            Console.Write("\\");
            Console.Write(arg2.Filename);
            Console.Write(":\t");
            Console.WriteLine(((ChangeJournalHandle.UsnReasonType)arg2.Record.Reason).ToString());
        }
        static void Main(string[] args)
        {
            string pathToVolumeToMonitor = @"\\?\C:";
            //This will filter to show only files that are deleted or created
            UsnReasonType reasonsToMonitor = UsnReasonType.USN_REASON_FILE_CREATE | UsnReasonType.USN_REASON_FILE_DELETE;
            using (ChangeJournalHandle cjh = new ChangeJournalHandle(pathToVolumeToMonitor))
            {
                cjh.OnChange += Cjh_OnChange;
                cjh.OnError += Cjh_OnError;
                cjh.EventTriggerMask = reasonsToMonitor;
                
                cjh.StartListening();
                Console.ReadLine();
                cjh.StopListening();
                

                cjh.OnChange -= Cjh_OnChange;
                cjh.OnError -= Cjh_OnError;
            }
        }
    }
}

Sample output:

\\?\C:\Windows\Temp\MSIc9528.LOG:       USN_REASON_FILE_CREATE

\\?\C:\Windows\Temp\MSIc9528.LOG:       USN_REASON_FILE_CREATE, USN_REASON_CLOSE

\\?\C:\Windows\Temp\MSIc9529.LOG:       USN_REASON_FILE_CREATE

\\?\C:\Windows\Temp\MSIc9529.LOG:       USN_REASON_FILE_CREATE, USN_REASON_CLOSE

\\?\C:\Windows\Temp\MSIc952a.LOG:       USN_REASON_FILE_CREATE

\\?\C:\Windows\Temp\MSIc952a.LOG:       USN_REASON_FILE_CREATE, USN_REASON_CLOSE

\\?\C:\Windows\Temp\MSIc952b.LOG:       USN_REASON_FILE_CREATE

\\?\C:\Windows\Temp\MSIc952b.LOG:       USN_REASON_FILE_CREATE, USN_REASON_CLOSE

 

 

 

Follow us on Twitter, www.twitter.com/WindowsSDK.

Comments (0)

Skip to main content