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);
}
}