Add Explorer Workbench with hierarchical, off-UI Storage analysis.
Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
209
src/Explorer.Windows/WindowsUsnJournal.cs
Normal file
209
src/Explorer.Windows/WindowsUsnJournal.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Explorer.Windows;
|
||||
|
||||
public sealed class WindowsUsnJournal : IUsnJournal
|
||||
{
|
||||
private readonly ILogger<WindowsUsnJournal> _logger;
|
||||
|
||||
public WindowsUsnJournal(ILogger<WindowsUsnJournal> logger) => _logger = logger;
|
||||
|
||||
public bool TryQuery(string rootPath, out UsnJournalState state, out string? error)
|
||||
{
|
||||
state = new UsnJournalState();
|
||||
error = null;
|
||||
var volume = OpenVolume(rootPath, out error);
|
||||
if (volume == nint.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var size = Marshal.SizeOf<NativeMethods.UsnJournalDataV2>();
|
||||
var buffer = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
if (!NativeMethods.DeviceIoControl(volume, NativeMethods.IoctlQueryUsnJournal, 0, 0, buffer, (uint)size, out _, 0))
|
||||
{
|
||||
error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = Marshal.PtrToStructure<NativeMethods.UsnJournalDataV2>(buffer);
|
||||
state = new UsnJournalState
|
||||
{
|
||||
JournalId = unchecked((long)data.UsnJournalId),
|
||||
NextUsn = data.NextUsn
|
||||
};
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
_logger.LogDebug(ex, "USN query failed for {Path}", rootPath);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.CloseHandle(volume);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<UsnRecord> Read(string rootPath, UsnJournalState from, int maxRecords, out UsnJournalState next, out UsnReadStatus status)
|
||||
{
|
||||
next = from;
|
||||
status = UsnReadStatus.Unavailable;
|
||||
var records = new List<UsnRecord>();
|
||||
if (!TryQuery(rootPath, out var current, out var error))
|
||||
{
|
||||
status = error?.Contains("denied", StringComparison.OrdinalIgnoreCase) == true
|
||||
? UsnReadStatus.AccessDenied
|
||||
: UsnReadStatus.Unavailable;
|
||||
return records;
|
||||
}
|
||||
|
||||
if (from.JournalId != 0 && from.JournalId != current.JournalId)
|
||||
{
|
||||
status = UsnReadStatus.JournalReset;
|
||||
next = current;
|
||||
return records;
|
||||
}
|
||||
|
||||
var volume = OpenVolume(rootPath, out _);
|
||||
if (volume == nint.Zero)
|
||||
{
|
||||
status = UsnReadStatus.AccessDenied;
|
||||
return records;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var read = new NativeMethods.ReadUsnJournalDataV1
|
||||
{
|
||||
StartUsn = from.NextUsn == 0 ? current.NextUsn : from.NextUsn,
|
||||
ReasonMask = 0xFFFFFFFF,
|
||||
ReturnOnlyOnClose = 0,
|
||||
Timeout = 0,
|
||||
BytesToWaitFor = 0,
|
||||
UsnJournalId = unchecked((ulong)current.JournalId),
|
||||
MinMajorVersion = 2,
|
||||
MaxMajorVersion = 3
|
||||
};
|
||||
|
||||
var inSize = Marshal.SizeOf(read);
|
||||
var inPtr = Marshal.AllocHGlobal(inSize);
|
||||
var outSize = 64 * 1024;
|
||||
var outPtr = Marshal.AllocHGlobal(outSize);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(read, inPtr, false);
|
||||
if (!NativeMethods.DeviceIoControl(volume, NativeMethods.IoctlReadUsnJournal, inPtr, (uint)inSize, outPtr, (uint)outSize, out var returned, 0))
|
||||
{
|
||||
var code = Marshal.GetLastWin32Error();
|
||||
status = code is 5 or 1314 ? UsnReadStatus.AccessDenied : UsnReadStatus.Error;
|
||||
if (code is 1179 or 1180)
|
||||
{
|
||||
status = UsnReadStatus.JournalReset;
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
if (returned < 8)
|
||||
{
|
||||
status = UsnReadStatus.Ok;
|
||||
next = current;
|
||||
return records;
|
||||
}
|
||||
|
||||
var nextUsn = Marshal.ReadInt64(outPtr);
|
||||
var offset = 8;
|
||||
while (offset + 60 < returned && records.Count < maxRecords)
|
||||
{
|
||||
var recordLength = Marshal.ReadInt32(outPtr, offset);
|
||||
if (recordLength <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var major = Marshal.ReadInt16(outPtr, offset + 4);
|
||||
var frn = Marshal.ReadInt64(outPtr, offset + 8);
|
||||
var parentFrn = Marshal.ReadInt64(outPtr, offset + 16);
|
||||
var usn = Marshal.ReadInt64(outPtr, offset + 24);
|
||||
var reason = Marshal.ReadInt32(outPtr, offset + 40);
|
||||
var attrs = Marshal.ReadInt32(outPtr, offset + 52);
|
||||
var nameLength = Marshal.ReadInt16(outPtr, offset + 56);
|
||||
var nameOffset = Marshal.ReadInt16(outPtr, offset + 58);
|
||||
var name = Marshal.PtrToStringUni(outPtr + offset + nameOffset, nameLength / 2) ?? "";
|
||||
records.Add(new UsnRecord
|
||||
{
|
||||
FileReferenceNumber = frn,
|
||||
ParentFileReferenceNumber = parentFrn,
|
||||
Usn = usn,
|
||||
FileName = name,
|
||||
Reason = reason,
|
||||
FileAttributes = attrs
|
||||
});
|
||||
offset += recordLength;
|
||||
_ = major;
|
||||
}
|
||||
|
||||
next = new UsnJournalState { JournalId = current.JournalId, NextUsn = nextUsn };
|
||||
status = UsnReadStatus.Ok;
|
||||
return records;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(inPtr);
|
||||
Marshal.FreeHGlobal(outPtr);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "USN read failed for {Path}", rootPath);
|
||||
status = UsnReadStatus.Error;
|
||||
return records;
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.CloseHandle(volume);
|
||||
}
|
||||
}
|
||||
|
||||
private static nint OpenVolume(string rootPath, out string? error)
|
||||
{
|
||||
error = null;
|
||||
var letter = Path.GetPathRoot(rootPath)?.TrimEnd('\\');
|
||||
if (string.IsNullOrEmpty(letter))
|
||||
{
|
||||
error = "Invalid volume";
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
var volumePath = @"\\.\" + letter;
|
||||
var handle = NativeMethods.CreateFile(
|
||||
volumePath,
|
||||
NativeMethods.GenericRead,
|
||||
NativeMethods.FileShareRead | NativeMethods.FileShareWrite,
|
||||
0,
|
||||
NativeMethods.OpenExisting,
|
||||
0,
|
||||
0);
|
||||
if (handle == nint.Zero || handle == new nint(-1))
|
||||
{
|
||||
error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user