Skip to content

Commit

Permalink
Add ChangeJournal.GetEntry
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Dec 16, 2024
1 parent 3a6f9ce commit 9e61dcf
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
var lastUsn = entries.OfType<ChangeJournalEntryVersion2or3>().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))
{
Expand Down
47 changes: 41 additions & 6 deletions src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -67,6 +68,40 @@ public IEnumerable<ChangeJournalEntry> 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<USN_RECORD_COMMON_HEADER>((nint)bufferPtr);
return (ChangeJournalEntryVersion2or3)ChangeJournalEntries.GetBufferedEntry((nint)bufferPtr, header);
}
}

public void RefreshJournalData()
{
Data = ReadJournalDataImpl();
Expand All @@ -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();
}
Expand All @@ -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();
}

Expand All @@ -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();
}

Expand All @@ -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();
}

Expand All @@ -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);
}
}
78 changes: 39 additions & 39 deletions src/Meziantou.Framework.Win32.ChangeJournal/ChangeJournalEntries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<USN_RECORD_V2>(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<USN_RECORD_V3>(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<USN_RECORD_V4>(bufferPointer);
var extendOffset = Marshal.OffsetOf<USN_RECORD_V4>(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<USN_RECORD_EXTENT>(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<ChangeJournalEntry>
{
private const int BufferSize = 8192;
Expand Down Expand Up @@ -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<USN_RECORD_V2>(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<USN_RECORD_V3>(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<USN_RECORD_V4>(bufferPointer);
var extendOffset = Marshal.OffsetOf<USN_RECORD_V4>(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<USN_RECORD_EXTENT>(extentPointer);
extents[i] = new ChangeJournalEntryExtent(extent);
}

return new ChangeJournalEntryVersion4(entry, extents);
}
else
{
throw new NotSupportedException($"Record version {header.MajorVersion}.{header.MinorVersion} is not supported");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(LatestTargetFrameworks)</TargetFrameworks>
<IsTrimmable>false</IsTrimmable>
<RootNamespace>Meziantou.Framework.Win32</RootNamespace>
<Version>3.1.1</Version>
<Version>3.2.0</Version>
<Description>Allow to access the Windows Change Journal to quickly detect changed files</Description>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
FILE_ID_INFO
WIN32_ERROR
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,39 @@ namespace Meziantou.Framework.Win32.Natives;
internal static class Win32DeviceControl
{
[SupportedOSPlatform("windows5.1.2600")]
internal static unsafe Span<byte> ControlWithInput<TStructure>(SafeFileHandle handle, Win32ControlCode code, ref TStructure structure, int bufferLength) where TStructure : struct
internal static unsafe Span<byte> ControlWithInput<TStructure>(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<byte>() : new byte[bufferLength];
bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
bufferPointer = bufferHandle.AddrOfPinnedObject();
var buffer = initialBufferLength is 0 ? Array.Empty<byte>() : 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);
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public void EnumerateEntries_ShouldFindNewFile()
changeJournal.Entries.OfType<ChangeJournalEntryVersion2or3>().FirstOrDefault(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal) && entry.Reason.HasFlag(ChangeReason.DataExtend)).Should().NotBeNull();
changeJournal.Entries.OfType<ChangeJournalEntryVersion2or3>().FirstOrDefault(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal) && entry.Reason.HasFlag(ChangeReason.Close)).Should().NotBeNull();

var lastUsn = changeJournal.Entries.OfType<ChangeJournalEntryVersion2or3>().Last(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal));
changeJournal.GetEntry(file).UniqueSequenceNumber.Should().Be(lastUsn.UniqueSequenceNumber);

File.Delete(file);
changeJournal.Entries.OfType<ChangeJournalEntryVersion2or3>().FirstOrDefault(entry => string.Equals(entry.Name, fileName, StringComparison.Ordinal) && entry.Reason.HasFlag(ChangeReason.FileDelete)).Should().NotBeNull();
});
Expand Down

0 comments on commit 9e61dcf

Please sign in to comment.