diff --git a/Samples/Meziantou.Framework.Win32.ChangeJournal.Sample/Program.cs b/Samples/Meziantou.Framework.Win32.ChangeJournal.Sample/Program.cs index 6d735ded..0ae0c9f6 100644 --- a/Samples/Meziantou.Framework.Win32.ChangeJournal.Sample/Program.cs +++ b/Samples/Meziantou.Framework.Win32.ChangeJournal.Sample/Program.cs @@ -11,6 +11,7 @@ var lastUsn = entries.OfType().LastOrDefault()?.UniqueSequenceNumber; Console.WriteLine($"Last USN: {lastUsn}"); +Console.WriteLine($"Last USN: {changeJournal.GetEntry("D:/test.txt").UniqueSequenceNumber}"); using (var fs = new FileStream("D:/test.txt", FileMode.Open, FileAccess.Write)) { diff --git a/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournal.cs b/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournal.cs index 47a47f61..c4d187c8 100644 --- a/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournal.cs +++ b/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournal.cs @@ -5,6 +5,7 @@ using Microsoft.Win32.SafeHandles; using Windows.Win32; using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.Ioctl; namespace Meziantou.Framework.Win32; @@ -67,6 +68,40 @@ public IEnumerable GetEntries(Usn currentUSN, ChangeReason r return new ChangeJournalEntries(this, new ReadChangeJournalOptions(currentUSN, reasonFilter, returnOnlyOnClose, timeout, _unprivileged)); } + public ChangeJournalEntryVersion2or3 GetEntry(string path) + { + using var handle = File.OpenHandle(path); + return GetEntry(handle); + } + + public unsafe ChangeJournalEntryVersion2or3 GetEntry(SafeFileHandle handle) + { + var buffer = new byte[USN_RECORD_V3.SizeOf(512)]; + fixed (void* bufferPtr = buffer) + { + uint returnedSize; + var controlResult = PInvoke.DeviceIoControl(handle, PInvoke.FSCTL_READ_FILE_USN_DATA, lpInBuffer: null, 0, bufferPtr, (uint)buffer.Length, &returnedSize, lpOverlapped: null); + if (!controlResult) + { + var errorCode = Marshal.GetLastWin32Error(); + if (errorCode == (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_MORE_DATA) + { + buffer = new byte[returnedSize]; + controlResult = PInvoke.DeviceIoControl(handle, PInvoke.FSCTL_READ_FILE_USN_DATA, lpInBuffer: null, 0, bufferPtr, (uint)buffer.Length, &returnedSize, lpOverlapped: null); + if (!controlResult) + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + else + { + throw new Win32Exception(errorCode); + } + } + + var header = Marshal.PtrToStructure((nint)bufferPtr); + return (ChangeJournalEntryVersion2or3)ChangeJournalEntries.GetBufferedEntry((nint)bufferPtr, header); + } + } + public void RefreshJournalData() { Data = ReadJournalDataImpl(); @@ -87,7 +122,7 @@ private JournalData ReadJournalDataImpl() return new JournalData(journalData); } - catch (Win32Exception ex) when (ex.NativeErrorCode == (int)Win32ErrorCode.ERROR_JOURNAL_NOT_ACTIVE) + catch (Win32Exception ex) when (ex.NativeErrorCode == (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_JOURNAL_NOT_ACTIVE) { return new JournalData(); } @@ -102,10 +137,10 @@ public void Delete(bool waitForCompletion) var deletionData = new Windows.Win32.System.Ioctl.DELETE_USN_JOURNAL_DATA { UsnJournalID = Data.ID, - DeleteFlags = waitForCompletion ? Windows.Win32.System.Ioctl.USN_DELETE_FLAGS.USN_DELETE_FLAG_NOTIFY : Windows.Win32.System.Ioctl.USN_DELETE_FLAGS.USN_DELETE_FLAG_DELETE, + DeleteFlags = waitForCompletion ? USN_DELETE_FLAGS.USN_DELETE_FLAG_NOTIFY : USN_DELETE_FLAGS.USN_DELETE_FLAG_DELETE, }; - Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.CreateUsnJournal, ref deletionData, bufferLength: 0); + Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.CreateUsnJournal, ref deletionData, initialBufferLength: 0); RefreshJournalData(); } @@ -117,7 +152,7 @@ public void Create(ulong maximumSize, ulong allocationDelta) MaximumSize = maximumSize, }; - Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.CreateUsnJournal, ref creationData, bufferLength: 0); + Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.CreateUsnJournal, ref creationData, initialBufferLength: 0); RefreshJournalData(); } @@ -129,7 +164,7 @@ public void Create(long maximumSize, long allocationDelta) MaximumSize = (ulong)maximumSize, }; - Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.CreateUsnJournal, ref creationData, bufferLength: 0); + Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.CreateUsnJournal, ref creationData, initialBufferLength: 0); RefreshJournalData(); } @@ -142,6 +177,6 @@ public void EnableTrackModifiedRanges(ulong chunkSize, long fileSizeThreshold) FileSizeThreshold = fileSizeThreshold, }; - Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.TrackModifiedRanges, ref trackData, bufferLength: 0); + Win32DeviceControl.ControlWithInput(ChangeJournalHandle, Win32ControlCode.TrackModifiedRanges, ref trackData, initialBufferLength: 0); } } diff --git a/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournalEntries.cs b/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournalEntries.cs index fc4e6321..48132e60 100644 --- a/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournalEntries.cs +++ b/src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournalEntries.cs @@ -30,6 +30,45 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } + internal static ChangeJournalEntry GetBufferedEntry(IntPtr bufferPointer, USN_RECORD_COMMON_HEADER header) + { + if (header is { MajorVersion: 2, MinorVersion: 0 }) + { + var entry = Marshal.PtrToStructure(bufferPointer); + var filenamePointer = bufferPointer + entry.FileNameOffset; + var name = Marshal.PtrToStringAuto(filenamePointer, entry.FileNameLength / 2); + Debug.Assert(name is not null); + return new ChangeJournalEntryVersion2or3(entry, name); + } + else if (header is { MajorVersion: 3, MinorVersion: 0 }) + { + var entry = Marshal.PtrToStructure(bufferPointer); + var filenamePointer = bufferPointer + entry.FileNameOffset; + var name = Marshal.PtrToStringAuto(filenamePointer, entry.FileNameLength / 2); + Debug.Assert(name is not null); + return new ChangeJournalEntryVersion2or3(entry, name); + } + else if (header is { MajorVersion: 4, MinorVersion: 0 }) + { + var entry = Marshal.PtrToStructure(bufferPointer); + var extendOffset = Marshal.OffsetOf(nameof(USN_RECORD_V4.Extents)); + + var extents = new ChangeJournalEntryExtent[entry.NumberOfExtents]; + for (int i = 0; i < entry.NumberOfExtents; i++) + { + var extentPointer = bufferPointer + extendOffset + i * entry.ExtentSize; + var extent = Marshal.PtrToStructure(extentPointer); + extents[i] = new ChangeJournalEntryExtent(extent); + } + + return new ChangeJournalEntryVersion4(entry, extents); + } + else + { + throw new NotSupportedException($"Record version {header.MajorVersion}.{header.MinorVersion} is not supported"); + } + } + private sealed class ChangeJournalEntriesEnumerator : IEnumerator { private const int BufferSize = 8192; @@ -136,44 +175,5 @@ private unsafe bool Read() return false; } } - - private static ChangeJournalEntry GetBufferedEntry(IntPtr bufferPointer, USN_RECORD_COMMON_HEADER header) - { - if (header is { MajorVersion: 2, MinorVersion: 0 }) - { - var entry = Marshal.PtrToStructure(bufferPointer); - var filenamePointer = bufferPointer + entry.FileNameOffset; - var name = Marshal.PtrToStringAuto(filenamePointer, entry.FileNameLength / 2); - Debug.Assert(name is not null); - return new ChangeJournalEntryVersion2or3(entry, name); - } - else if (header is { MajorVersion: 3, MinorVersion: 0 }) - { - var entry = Marshal.PtrToStructure(bufferPointer); - var filenamePointer = bufferPointer + entry.FileNameOffset; - var name = Marshal.PtrToStringAuto(filenamePointer, entry.FileNameLength / 2); - Debug.Assert(name is not null); - return new ChangeJournalEntryVersion2or3(entry, name); - } - else if (header is { MajorVersion: 4, MinorVersion: 0 }) - { - var entry = Marshal.PtrToStructure(bufferPointer); - var extendOffset = Marshal.OffsetOf(nameof(USN_RECORD_V4.Extents)); - - var extents = new ChangeJournalEntryExtent[entry.NumberOfExtents]; - for (int i = 0; i < entry.NumberOfExtents; i++) - { - var extentPointer = bufferPointer + extendOffset + i * entry.ExtentSize; - var extent = Marshal.PtrToStructure(extentPointer); - extents[i] = new ChangeJournalEntryExtent(extent); - } - - return new ChangeJournalEntryVersion4(entry, extents); - } - else - { - throw new NotSupportedException($"Record version {header.MajorVersion}.{header.MinorVersion} is not supported"); - } - } } } diff --git a/src/Meziantou.Framework.Win32.ChangeJournal/Meziantou.Framework.Win32.ChangeJournal.csproj b/src/Meziantou.Framework.Win32.ChangeJournal/Meziantou.Framework.Win32.ChangeJournal.csproj index 53d11205..b395dae7 100644 --- a/src/Meziantou.Framework.Win32.ChangeJournal/Meziantou.Framework.Win32.ChangeJournal.csproj +++ b/src/Meziantou.Framework.Win32.ChangeJournal/Meziantou.Framework.Win32.ChangeJournal.csproj @@ -1,10 +1,10 @@ - + $(LatestTargetFrameworks) false Meziantou.Framework.Win32 - 3.1.1 + 3.2.0 Allow to access the Windows Change Journal to quickly detect changed files diff --git a/src/Meziantou.Framework.Win32.ChangeJournal/NativeMethods.txt b/src/Meziantou.Framework.Win32.ChangeJournal/NativeMethods.txt index 509ada48..d763260e 100644 --- a/src/Meziantou.Framework.Win32.ChangeJournal/NativeMethods.txt +++ b/src/Meziantou.Framework.Win32.ChangeJournal/NativeMethods.txt @@ -12,5 +12,7 @@ Windows.Win32.System.Ioctl.USN_RECORD_V3 Windows.Win32.System.Ioctl.USN_RECORD_V4 Windows.Win32.System.Ioctl.USN_TRACK_MODIFIED_RANGES FLAG_USN_TRACK_MODIFIED_RANGES_ENABLE +FSCTL_READ_FILE_USN_DATA GetFileInformationByHandleEx -FILE_ID_INFO \ No newline at end of file +FILE_ID_INFO +WIN32_ERROR \ No newline at end of file diff --git a/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32DeviceControl.cs b/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32DeviceControl.cs index d3b7c61f..0b468cc0 100644 --- a/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32DeviceControl.cs +++ b/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32DeviceControl.cs @@ -11,31 +11,39 @@ namespace Meziantou.Framework.Win32.Natives; internal static class Win32DeviceControl { [SupportedOSPlatform("windows5.1.2600")] - internal static unsafe Span ControlWithInput(SafeFileHandle handle, Win32ControlCode code, ref TStructure structure, int bufferLength) where TStructure : struct + internal static unsafe Span ControlWithInput(SafeFileHandle handle, Win32ControlCode code, ref TStructure structure, int initialBufferLength) where TStructure : struct { uint returnedSize; bool controlResult; - GCHandle bufferHandle; - IntPtr bufferPointer; - var buffer = bufferLength is 0 ? Array.Empty() : new byte[bufferLength]; - bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - bufferPointer = bufferHandle.AddrOfPinnedObject(); + var buffer = initialBufferLength is 0 ? Array.Empty() : new byte[initialBufferLength]; var structurePointer = Unsafe.AsPointer(ref structure); - try + fixed (void* bufferPointer = buffer) { - controlResult = PInvoke.DeviceIoControl(handle, (uint)code, structurePointer, (uint)Marshal.SizeOf(structure), (void*)bufferPointer, (uint)buffer.Length, &returnedSize, lpOverlapped: null); - } - finally - { - bufferHandle.Free(); + controlResult = PInvoke.DeviceIoControl(handle, (uint)code, structurePointer, (uint)Marshal.SizeOf(structure), bufferPointer, (uint)buffer.Length, &returnedSize, lpOverlapped: null); } if (!controlResult) - throw new Win32Exception(Marshal.GetLastWin32Error()); + { + var errorCode = Marshal.GetLastWin32Error(); + if (errorCode == (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_MORE_DATA) + { + buffer = new byte[returnedSize]; + fixed (void* bufferPointer = buffer) + { + controlResult = PInvoke.DeviceIoControl(handle, (uint)code, structurePointer, (uint)Marshal.SizeOf(structure), bufferPointer, (uint)buffer.Length, &returnedSize, lpOverlapped: null); + } + + if (!controlResult) + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + else + { + throw new Win32Exception(errorCode); + } + } - Debug.Assert(returnedSize <= bufferLength); return buffer.AsSpan(0, (int)returnedSize); } diff --git a/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32ErrorCode.cs b/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32ErrorCode.cs deleted file mode 100644 index ab68d1a8..00000000 --- a/src/Meziantou.Framework.Win32.ChangeJournal/Natives/Win32ErrorCode.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Meziantou.Framework.Win32.Natives; - -internal enum Win32ErrorCode -{ - INVALID_HANDLE_VALUE = -1, - ERROR_JOURNAL_NOT_ACTIVE = 1179, -} diff --git a/tests/Meziantou.Framework.Win32.ChangeJournal.Tests/ChangeJournalTests.cs b/tests/Meziantou.Framework.Win32.ChangeJournal.Tests/ChangeJournalTests.cs index 90d19449..9f46ad94 100644 --- a/tests/Meziantou.Framework.Win32.ChangeJournal.Tests/ChangeJournalTests.cs +++ b/tests/Meziantou.Framework.Win32.ChangeJournal.Tests/ChangeJournalTests.cs @@ -24,6 +24,9 @@ public void EnumerateEntries_ShouldFindNewFile() changeJournal.Entries.OfType().FirstOrDefault(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal) && entry.Reason.HasFlag(ChangeReason.DataExtend)).Should().NotBeNull(); changeJournal.Entries.OfType().FirstOrDefault(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal) && entry.Reason.HasFlag(ChangeReason.Close)).Should().NotBeNull(); + var lastUsn = changeJournal.Entries.OfType().Last(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal)); + changeJournal.GetEntry(file).UniqueSequenceNumber.Should().Be(lastUsn.UniqueSequenceNumber); + File.Delete(file); changeJournal.Entries.OfType().FirstOrDefault(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal) && entry.Reason.HasFlag(ChangeReason.FileDelete)).Should().NotBeNull(); });