using System.Buffers.Binary; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; using Microsoft.Win32.SafeHandles; namespace FluidExplorer.Services.Search.Usn; /// /// 增量监听的回退通道:ReadDirectoryChangesW。 /// /// 什么情况下用:USN 变更日志不可用(卷上没日志且没权限创建、被策略禁用、日志刚被删除等)。 /// 这条路径不需要任何特殊权限,普通用户也能跑,从而保证「索引实时」这个卖点不落空。 /// /// 与 USN 的差别与应对: /// * RDCW 只给相对路径,不给 FRN。这里用 CreateFileW(FILE_READ_ATTRIBUTES) + /// GetFileInformationByHandle 取句柄上的 FileIndex —— 它与 USN 的 FRN 完全同口径 /// (低 48 位记录号 + 高 16 位序列号),因此能直接命中同一个索引条目。 /// * 新增/改名/内容变化都伴随着文件仍然存在,可以 stat 出来 → 直接 upsert。 /// * 删除只剩一个路径(stat 必然失败),无法反查 FRN。应对:把受影响的目录记下来, /// 批处理结束后对该目录做一次「磁盘现状 vs 索引」的对账(), /// 只把索引里存在、磁盘上已消失的孩子打墓碑。对账按目录去重、每轮有上限,代价可控。 /// internal sealed class DirectoryChangeWatcher { private const int ReadBufferSize = 64 * 1024; /// 每轮最多对账多少个目录,防止一次批量删除把 CPU 打满。 private const int MaxResyncDirectoriesPerPass = 256; private static readonly uint NotifyFilter = UsnNative.FILE_NOTIFY_CHANGE_FILE_NAME | UsnNative.FILE_NOTIFY_CHANGE_DIR_NAME | UsnNative.FILE_NOTIFY_CHANGE_SIZE | UsnNative.FILE_NOTIFY_CHANGE_LAST_WRITE | UsnNative.FILE_NOTIFY_CHANGE_ATTRIBUTES; private readonly UsnVolumeIndex _index; private readonly string _root; private readonly ConcurrentDictionary _directoryFrnCache = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _pendingResync = new(StringComparer.OrdinalIgnoreCase); private readonly List _resyncScratch = []; private SafeFileHandle? _handle; private volatile bool _stopRequested; internal DirectoryChangeWatcher(UsnVolumeIndex index) { _index = index; _root = index.VolumeRoot; } /// 打断阻塞中的 ReadDirectoryChangesW:置停止标志 + 关目录句柄(双保险)。 internal void RequestStop() { _stopRequested = true; Interlocked.Exchange(ref _handle, null)?.Dispose(); } /// 打开卷根目录句柄。返回 false 时 给出中文原因。 internal bool TryOpen(out string? error) { error = null; var handle = UsnNative.CreateFileW( _root, UsnNative.FILE_LIST_DIRECTORY, UsnNative.FILE_SHARE_READ | UsnNative.FILE_SHARE_WRITE | UsnNative.FILE_SHARE_DELETE, IntPtr.Zero, UsnNative.OPEN_EXISTING, UsnNative.FILE_FLAG_BACKUP_SEMANTICS, // 打开目录必须带这个 IntPtr.Zero); if (handle.IsInvalid) { int openError = Marshal.GetLastWin32Error(); handle.Dispose(); error = UsnNative.DescribeError(openError, $"无法以 FILE_LIST_DIRECTORY 打开 {_root}"); return false; } Volatile.Write(ref _handle, handle); return true; } /// 阻塞式监听循环,直到 或句柄被关闭。 internal void Run() { var handle = Volatile.Read(ref _handle); if (handle is null) return; var buffer = new byte[ReadBufferSize]; try { while (!_stopRequested) { uint returned; bool ok; unsafe { fixed (byte* p = buffer) { ok = UsnNative.ReadDirectoryChangesW( handle, p, (uint)buffer.Length, true, NotifyFilter, out returned, IntPtr.Zero, IntPtr.Zero); } } if (!ok) { int readError = Marshal.GetLastWin32Error(); if (_stopRequested || readError is UsnNative.ERROR_OPERATION_ABORTED or UsnNative.ERROR_CANCELLED or UsnNative.ERROR_INVALID_HANDLE) { break; } // 单次失败不放弃:报给 UI 后继续(例如枚举期间目录被临时独占) _index.ReportWatchIssue($"ReadDirectoryChangesW 出错:{UsnNative.DescribeError(readError)}"); continue; } if (returned == 0) { // 变更缓冲区溢出:期间的事件已经丢了,只能提示 UI 做一次重建 _index.ReportWatchIssue("变更缓冲区溢出,部分变更已丢失,建议重建索引以保持一致。"); continue; } ProcessEvents(buffer.AsSpan(0, (int)returned)); FlushPendingResync(); } } finally { Interlocked.Exchange(ref _handle, null)?.Dispose(); } } private void ProcessEvents(ReadOnlySpan buffer) { int offset = 0; while (true) { if (offset + UsnNative.FileNotifyInformation.HeaderSize > buffer.Length) break; uint nextEntry = BinaryPrimitives.ReadUInt32LittleEndian(buffer[offset..]); uint action = BinaryPrimitives.ReadUInt32LittleEndian(buffer[(offset + 4)..]); int nameBytes = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer[(offset + 8)..]); int nameOffset = offset + UsnNative.FileNotifyInformation.HeaderSize; if (nameBytes <= 0 || nameOffset + nameBytes > buffer.Length) break; // 文件名是相对被监听目录(这里是卷根)的路径,形如 "Users\Public\x.txt",不以 NUL 结尾 var relative = MemoryMarshal.Cast(buffer.Slice(nameOffset, nameBytes)); Handle(relative, action); if (nextEntry == 0) break; offset += (int)nextEntry; } } private void Handle(ReadOnlySpan relative, uint action) { if (relative.Length == 0) return; var fullPath = string.Concat(_root, relative); switch (action) { case UsnNative.FILE_ACTION_ADDED: case UsnNative.FILE_ACTION_RENAMED_NEW_NAME: case UsnNative.FILE_ACTION_MODIFIED: UpsertFromDisk(fullPath); break; case UsnNative.FILE_ACTION_REMOVED: case UsnNative.FILE_ACTION_RENAMED_OLD_NAME: QueueDirectoryResync(fullPath); break; } } /// 按路径 stat 出 FRN + 元数据,然后按 FRN 落入索引(存在则更新,不存在则新增)。 private void UpsertFromDisk(string fullPath) { if (!UsnNative.TryStatPath(fullPath, out var info)) return; // 文件已再次消失等,忽略 var name = Path.GetFileName(fullPath.AsSpan()); if (name.Length == 0) return; ulong parentFrn = 0; var parentPath = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(parentPath) && !parentPath.Equals(_root, StringComparison.OrdinalIgnoreCase)) { parentFrn = GetDirectoryFrn(parentPath); } bool isDirectory = (info.FileAttributes & UsnNative.FILE_ATTRIBUTE_DIRECTORY) != 0; _index.UpsertFromFileSystem( info.FileIndex, parentFrn, name, isDirectory, isDirectory ? 0 : info.FileSize, UsnVolumeIndex.FileTimeToTicks(info.LastWriteTime.ToInt64()), info.FileAttributes); if (isDirectory) _directoryFrnCache[fullPath] = UsnNative.NormalizeFrn(info.FileIndex); } private ulong GetDirectoryFrn(string directoryPath) { if (_directoryFrnCache.TryGetValue(directoryPath, out var cached)) return cached; if (!UsnNative.TryStatPath(directoryPath, out var info)) return 0; var frn = UsnNative.NormalizeFrn(info.FileIndex); _directoryFrnCache[directoryPath] = frn; return frn; } private void QueueDirectoryResync(string fullPath) { var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(directory)) _pendingResync.Add(directory); } private void FlushPendingResync() { if (_pendingResync.Count == 0) return; _resyncScratch.Clear(); foreach (var directory in _pendingResync) { _resyncScratch.Add(directory); if (_resyncScratch.Count >= MaxResyncDirectoriesPerPass) break; } int removed = 0; foreach (var directory in _resyncScratch) { _pendingResync.Remove(directory); removed += _index.ResyncDirectoryFromDisk(directory); } if (removed > 0) _index.ReportWatchIssue(null, removed); } }