Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace FluidExplorer.Services.Search.Usn;
|
||||
|
||||
/// <summary>
|
||||
/// 增量监听的<b>回退通道</b>:ReadDirectoryChangesW。
|
||||
///
|
||||
/// 什么情况下用:USN 变更日志不可用(卷上没日志且没权限创建、被策略禁用、日志刚被删除等)。
|
||||
/// 这条路径不需要任何特殊权限,普通用户也能跑,从而保证「索引实时」这个卖点不落空。
|
||||
///
|
||||
/// 与 USN 的差别与应对:
|
||||
/// * RDCW 只给相对<b>路径</b>,不给 FRN。这里用 CreateFileW(FILE_READ_ATTRIBUTES) +
|
||||
/// GetFileInformationByHandle 取句柄上的 FileIndex —— 它与 USN 的 FRN 完全同口径
|
||||
/// (低 48 位记录号 + 高 16 位序列号),因此能直接命中同一个索引条目。
|
||||
/// * 新增/改名/内容变化都伴随着文件仍然存在,可以 stat 出来 → 直接 upsert。
|
||||
/// * 删除只剩一个路径(stat 必然失败),无法反查 FRN。应对:把受影响的<b>目录</b>记下来,
|
||||
/// 批处理结束后对该目录做一次「磁盘现状 vs 索引」的对账(<see cref="UsnVolumeIndex.ResyncDirectoryFromDisk"/>),
|
||||
/// 只把索引里存在、磁盘上已消失的孩子打墓碑。对账按目录去重、每轮有上限,代价可控。
|
||||
/// </summary>
|
||||
internal sealed class DirectoryChangeWatcher
|
||||
{
|
||||
private const int ReadBufferSize = 64 * 1024;
|
||||
|
||||
/// <summary>每轮最多对账多少个目录,防止一次批量删除把 CPU 打满。</summary>
|
||||
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<string, ulong> _directoryFrnCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _pendingResync = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<string> _resyncScratch = [];
|
||||
|
||||
private SafeFileHandle? _handle;
|
||||
private volatile bool _stopRequested;
|
||||
|
||||
internal DirectoryChangeWatcher(UsnVolumeIndex index)
|
||||
{
|
||||
_index = index;
|
||||
_root = index.VolumeRoot;
|
||||
}
|
||||
|
||||
/// <summary>打断阻塞中的 ReadDirectoryChangesW:置停止标志 + 关目录句柄(双保险)。</summary>
|
||||
internal void RequestStop()
|
||||
{
|
||||
_stopRequested = true;
|
||||
Interlocked.Exchange(ref _handle, null)?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>打开卷根目录句柄。返回 false 时 <paramref name="error"/> 给出中文原因。</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>阻塞式监听循环,直到 <see cref="RequestStop"/> 或句柄被关闭。</summary>
|
||||
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<byte> 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<byte, char>(buffer.Slice(nameOffset, nameBytes));
|
||||
Handle(relative, action);
|
||||
|
||||
if (nextEntry == 0) break;
|
||||
offset += (int)nextEntry;
|
||||
}
|
||||
}
|
||||
|
||||
private void Handle(ReadOnlySpan<char> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>按路径 stat 出 FRN + 元数据,然后按 FRN 落入索引(存在则更新,不存在则新增)。</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user