using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using FastSeekWpf.NativeInterop; namespace FastSeekWpf.Core; public class UsnWatcher : IDisposable { private readonly IntPtr _handle; private readonly NtfsDrive _drive; private readonly Action _onEvent; private bool _disposed; private bool _running; public long NextUsn { get; private set; } public ulong JournalId { get; private set; } public UsnWatcher(NtfsDrive drive, Action onEvent, JournalCheckpoint? checkpoint) { _drive = drive; _onEvent = onEvent; _handle = Win32Api.CreateFileW( drive.DevicePath, 0, Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE, IntPtr.Zero, Win32Api.OPEN_EXISTING, Win32Api.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero); if (_handle == new IntPtr(-1)) throw new IOException($"Failed to open drive {drive.Letter}:"); var journalData = new USN_JOURNAL_DATA_V0(); IntPtr outBuf = Marshal.AllocHGlobal(Marshal.SizeOf()); try { bool ok = Win32Api.DeviceIoControl( _handle, Win32Api.FSCTL_QUERY_USN_JOURNAL, IntPtr.Zero, 0, outBuf, (uint)Marshal.SizeOf(), out uint bytesReturned, IntPtr.Zero); if (!ok) throw new IOException("Failed to query USN journal"); journalData = Marshal.PtrToStructure(outBuf); } finally { Marshal.FreeHGlobal(outBuf); } if (checkpoint != null) { if (checkpoint.JournalId != journalData.UsnJournalID) throw new IOException("Journal ID mismatch — rescan needed"); if (checkpoint.NextUsn < journalData.FirstUsn || checkpoint.NextUsn > journalData.NextUsn) throw new IOException("Saved USN outside journal range — rescan needed"); NextUsn = checkpoint.NextUsn; } else { NextUsn = journalData.NextUsn; } JournalId = journalData.UsnJournalID; } public static UsnWatcher CreateForCheckpoint(NtfsDrive drive, Action onEvent, JournalCheckpoint checkpoint) { return new UsnWatcher(drive, onEvent, checkpoint); } public JournalCheckpoint Checkpoint() { return new JournalCheckpoint { NextUsn = NextUsn, JournalId = JournalId, DriveLetter = _drive.Letter }; } public int Drain() { int count = 0; while (true) { long before = NextUsn; Poll(); if (NextUsn == before) break; count++; } return count; } /// Synchronous run loop — matches Rust UsnWatcher::run() public void Run() { byte[] buffer = new byte[65536]; while (!_disposed) { Thread.Sleep(500); Poll(); } } /// Synchronous run with shared checkpoint updates — matches Rust UsnWatcher::run_shared() public void RunShared(List sharedCheckpoints) { byte[] buffer = new byte[65536]; while (!_disposed) { Thread.Sleep(500); Poll(); lock (sharedCheckpoints) { sharedCheckpoints.RemoveAll(c => c.DriveLetter == _drive.Letter); sharedCheckpoints.Add(Checkpoint()); } } } public async Task RunAsync(CancellationToken ct) { _running = true; byte[] buffer = new byte[65536]; while (!_disposed && !ct.IsCancellationRequested && _running) { Poll(); await Task.Delay(500, ct); } } private void Poll() { var readData = new READ_USN_JOURNAL_DATA_V0 { StartUsn = NextUsn, ReasonMask = Win32Api.USN_REASON_FILE_CREATE | Win32Api.USN_REASON_FILE_DELETE | Win32Api.USN_REASON_RENAME_NEW_NAME | Win32Api.USN_REASON_RENAME_OLD_NAME, ReturnOnlyOnClose = 0, Timeout = 0, BytesToWaitFor = 0, UsnJournalID = JournalId }; int readDataSize = Marshal.SizeOf(); IntPtr readDataPtr = Marshal.AllocHGlobal(readDataSize); IntPtr bufferPtr = Marshal.AllocHGlobal(65536); try { Marshal.StructureToPtr(readData, readDataPtr, false); bool ok = Win32Api.DeviceIoControl( _handle, Win32Api.FSCTL_READ_USN_JOURNAL, readDataPtr, (uint)readDataSize, bufferPtr, 65536, out uint bytesReturned, IntPtr.Zero); if (!ok || bytesReturned <= 8) return; NextUsn = Marshal.ReadInt64(bufferPtr); int offset = 8; while (offset + Marshal.SizeOf() <= (int)bytesReturned) { IntPtr recordPtr = IntPtr.Add(bufferPtr, offset); var record = Marshal.PtrToStructure(recordPtr); if (record.RecordLength == 0) break; ProcessRecord(record, bufferPtr, offset); offset += (int)record.RecordLength; } } finally { Marshal.FreeHGlobal(readDataPtr); Marshal.FreeHGlobal(bufferPtr); } } private void ProcessRecord(USN_RECORD_V2 record, IntPtr bufferBase, int offset) { int nameOffset = offset + (int)record.FileNameOffset; int nameLen = record.FileNameLength / 2; var nameChars = new char[nameLen]; for (int i = 0; i < nameLen; i++) nameChars[i] = (char)Marshal.ReadInt16(bufferBase, nameOffset + i * 2); string name = new string(nameChars); bool isDir = (record.FileAttributes & 0x10) != 0; ulong fileRef = record.FileReferenceNumber; ulong parentRef = record.ParentFileReferenceNumber; uint reason = record.Reason; var kind = isDir ? FileKind.Directory : FileKind.File; if ((reason & Win32Api.USN_REASON_FILE_DELETE) != 0) { _onEvent(new IndexEvent.Deleted(fileRef)); return; } if ((reason & Win32Api.USN_REASON_RENAME_NEW_NAME) != 0) { _onEvent(new IndexEvent.Moved(fileRef, parentRef, name, kind)); return; } if ((reason & Win32Api.USN_REASON_FILE_CREATE) != 0) { _onEvent(new IndexEvent.Created(new FileRecord { FileRef = fileRef, ParentRef = parentRef, Name = name, Kind = kind })); } } public void Dispose() { if (!_disposed) { _running = false; Win32Api.CloseHandle(_handle); _disposed = true; } GC.SuppressFinalize(this); } ~UsnWatcher() => Dispose(); }