App.xaml.cs: Update to use IndexStore.Finalize() and index.Name(entry) APIs. Fix benchmark to use Entries list instead of removed NameCache property.
cee26fa verified | using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using System.Threading; | |
| using System.Windows; | |
| using System.Windows.Interop; | |
| using FastSeekWpf.Core; | |
| using FastSeekWpf.NativeInterop; | |
| namespace FastSeekWpf; | |
| public partial class App : Application | |
| { | |
| private const string MutexName = "FastSeekWpf_SingleInstance_Mutex"; | |
| private const uint WM_TOGGLE_WINDOW = Win32Api.WM_USER + 3; | |
| private Mutex? _mutex; | |
| private bool _ownsMutex; | |
| private volatile bool _shutdown; | |
| private MainWindow? _mainWindow; | |
| private Thread? _hotkeyThread; | |
| [] | |
| private static extern bool AttachConsole(int dwProcessId); | |
| [] | |
| private static extern bool AllocConsole(); | |
| [] | |
| private static extern bool FreeConsole(); | |
| private const int ATTACH_PARENT_PROCESS = -1; | |
| private void OnStartup(object sender, StartupEventArgs e) | |
| { | |
| if (e.Args.Contains("--cli")) | |
| { | |
| RunCliMode(); | |
| Shutdown(); | |
| return; | |
| } | |
| if (e.Args.Contains("--clear-cache")) | |
| { | |
| RunClearCache(); | |
| Shutdown(); | |
| return; | |
| } | |
| Logger.Log("=== App startup ==="); | |
| _mutex = new Mutex(true, MutexName, out bool createdNew); | |
| if (!createdNew) | |
| { | |
| IntPtr hwnd = Win32Api.FindWindowW(null, "FastSeek"); | |
| if (hwnd != IntPtr.Zero) | |
| Win32Api.PostMessageW(hwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero); | |
| Shutdown(); | |
| return; | |
| } | |
| _ownsMutex = true; | |
| _mainWindow = new MainWindow(); | |
| _mainWindow.Show(); | |
| _hotkeyThread = new Thread(HotkeyLoop) | |
| { | |
| IsBackground = true, | |
| Name = "HotkeyThread" | |
| }; | |
| var helper = new WindowInteropHelper(_mainWindow); | |
| _hotkeyThread.Start(helper.Handle); | |
| } | |
| private static void EnsureConsole() | |
| { | |
| if (!AttachConsole(ATTACH_PARENT_PROCESS)) | |
| AllocConsole(); | |
| Console.OutputEncoding = Encoding.UTF8; | |
| var stdout = Console.OpenStandardOutput(); | |
| var writer = new StreamWriter(stdout, Encoding.UTF8) { AutoFlush = true }; | |
| Console.SetOut(writer); | |
| Console.SetError(writer); | |
| } | |
| private static void RunClearCache() | |
| { | |
| EnsureConsole(); | |
| var cachePath = Path.Combine( | |
| Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), | |
| "FastSeek", "cache.dat"); | |
| if (File.Exists(cachePath)) | |
| { | |
| var size = new FileInfo(cachePath).Length; | |
| File.Delete(cachePath); | |
| Console.WriteLine($"Cache cleared ({size:N0} bytes)"); | |
| } | |
| else | |
| { | |
| Console.WriteLine("No cache found."); | |
| } | |
| Console.WriteLine("Press Enter..."); | |
| Console.ReadLine(); | |
| FreeConsole(); | |
| } | |
| private static void RunCliMode() | |
| { | |
| EnsureConsole(); | |
| Console.Clear(); | |
| PrintHeader("FastSeek CLI"); | |
| Console.WriteLine(); | |
| bool elevated = Elevation.IsElevated(); | |
| if (elevated) | |
| PrintLine(ConsoleColor.Green, "Elevation", "Administrator β"); | |
| else | |
| { | |
| PrintLine(ConsoleColor.Red, "Elevation", $"{Elevation.StatusText()} β"); | |
| PrintError("NOT RUNNING AS ADMINISTRATOR β MFT read will fail."); | |
| PrintInfo("To fix: Build the project, then run the compiled .exe:"); | |
| PrintDetail(" dotnet publish -c Release -r win-x64"); | |
| PrintDetail(" .\\bin\\Release\\net8.0-windows\\win-x64\\FastSeekWpf.exe --cli"); | |
| Console.WriteLine(); | |
| } | |
| var drives = DriveDiscovery.GetNtfsDrives(); | |
| PrintLine(ConsoleColor.Cyan, "Drives", $"{drives.Count} NTFS found"); | |
| foreach (var d in drives) PrintDetail($" {d.Letter}: {d.Root}"); | |
| Console.WriteLine(); | |
| if (drives.Count == 0) | |
| { | |
| PrintError("No NTFS drives found."); | |
| if (!elevated) | |
| PrintInfo("This is likely because you are not elevated. Try running as Administrator."); | |
| WaitExit(); | |
| return; | |
| } | |
| if (!elevated) | |
| PrintInfo("Attempting scan anyway (will likely fail without elevation)..."); | |
| PrintLine(ConsoleColor.Cyan, "Scan", "Reading MFT..."); | |
| var index = new IndexStore(); | |
| long totalRecords = 0; | |
| var sw = Stopwatch.StartNew(); | |
| foreach (var drive in drives) | |
| { | |
| try | |
| { | |
| using var reader = new MftReader(drive); | |
| var (scan, method) = reader.ScanAny(); | |
| if (scan.Records.Count > 0) | |
| { | |
| index.PopulateFromScan(scan, drive.Root); | |
| totalRecords += scan.Records.Count; | |
| PrintLine(ConsoleColor.Green, $" {drive.Letter}", $"{scan.Records.Count:N0} records ({method})"); | |
| } | |
| else | |
| { | |
| PrintError($" {drive.Letter}: 0 records"); | |
| if (!elevated) | |
| PrintInfo(" β Expected when not running as Administrator."); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| PrintError($" {drive.Letter}: {ex.Message}"); | |
| } | |
| } | |
| index.Finalize(); | |
| sw.Stop(); | |
| PrintLine(ConsoleColor.White, "Index", $"{index.Count:N0} entries | {sw.ElapsedMilliseconds}ms | {totalRecords / Math.Max(sw.ElapsedMilliseconds, 1):N0} rec/ms"); | |
| Console.WriteLine(); | |
| if (index.Count > 0) | |
| { | |
| var cacheSw = Stopwatch.StartNew(); | |
| CacheManager.SaveCache(index); | |
| cacheSw.Stop(); | |
| var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FastSeek", "cache.dat"); | |
| long cacheSize = File.Exists(cachePath) ? new FileInfo(cachePath).Length : 0; | |
| PrintLine(ConsoleColor.Cyan, "Cache", $"saved {FormatBytes(cacheSize)} in {cacheSw.ElapsedMilliseconds}ms"); | |
| } | |
| else | |
| PrintLine(ConsoleColor.DarkGray, "Cache", "skipped (empty index)"); | |
| Console.WriteLine(); | |
| if (index.Count > 0) | |
| { | |
| PrintLine(ConsoleColor.Cyan, "Benchmark", "1,000 random queries..."); | |
| var benchSw = Stopwatch.StartNew(); | |
| var rnd = new Random(42); | |
| var exclusions = CacheManager.LoadExclusions(); | |
| long benchResults = 0; | |
| for (int i = 0; i < 1000; i++) | |
| { | |
| if (index.Entries.Count == 0) break; | |
| var entry = index.Entries[rnd.Next(index.Entries.Count)]; | |
| var name = index.Name(entry); | |
| if (name.Length < 2) continue; | |
| int start = rnd.Next(name.Length - 1); | |
| int len = rnd.Next(1, Math.Min(name.Length - start, 6)); | |
| var q = name.Substring(start, len).ToLowerInvariant(); | |
| var r = SearchEngine.Search(index, q, 50, false, exclusions); | |
| benchResults += r.Count; | |
| } | |
| benchSw.Stop(); | |
| double qps = 1000.0 / Math.Max(benchSw.ElapsedMilliseconds, 1) * 1000; | |
| PrintLine(ConsoleColor.White, "Result", $"{benchSw.ElapsedMilliseconds}ms total | {qps:N0} qps | {benchResults / 1000.0:F1} avg results"); | |
| } | |
| else | |
| PrintLine(ConsoleColor.DarkGray, "Benchmark", "skipped (empty index)"); | |
| Console.WriteLine(); | |
| PrintDivider(); | |
| Console.WriteLine(); | |
| if (index.Count == 0) | |
| { | |
| PrintError("INDEX IS EMPTY β GUI will show no results."); | |
| if (!elevated) | |
| { | |
| PrintInfo("ROOT CAUSE: You must run the COMPILED .exe as Administrator."); | |
| PrintInfo("'dotnet run' does NOT trigger UAC elevation."); | |
| PrintInfo("Steps:"); | |
| PrintDetail(" 1. dotnet publish -c Release -r win-x64"); | |
| PrintDetail(" 2. .\\bin\\Release\\net8.0-windows\\win-x64\\FastSeekWpf.exe --cli"); | |
| PrintDetail(" 3. Accept the UAC prompt"); | |
| } | |
| else | |
| { | |
| PrintInfo("Possible causes:"); | |
| PrintDetail(" β’ MFT direct read blocked by antivirus/EDR"); | |
| PrintDetail(" β’ Volume is ReFS/BitLocker/VM shared folder"); | |
| PrintDetail(" β’ chkdsk / Windows Update holding volume lock"); | |
| } | |
| Console.WriteLine(); | |
| } | |
| if (index.Count > 0) | |
| { | |
| PrintHeader("Interactive Search (type 'exit' to quit)"); | |
| Console.WriteLine(); | |
| while (true) | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.Write("> "); | |
| Console.ResetColor(); | |
| var query = Console.ReadLine()?.Trim(); | |
| if (string.IsNullOrEmpty(query)) continue; | |
| if (query.Equals("exit", StringComparison.OrdinalIgnoreCase)) break; | |
| var searchSw = Stopwatch.StartNew(); | |
| var exclusions = CacheManager.LoadExclusions(); | |
| var results = SearchEngine.Search(index, query, 20, false, exclusions); | |
| searchSw.Stop(); | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.WriteLine($" {results.Count} results | {searchSw.Elapsed.TotalMilliseconds:F3}ms"); | |
| Console.ResetColor(); | |
| foreach (var r in results.Take(10)) | |
| { | |
| var color = r.Kind switch | |
| { | |
| ResultKind.App => ConsoleColor.Green, | |
| ResultKind.Document => ConsoleColor.Yellow, | |
| ResultKind.Image => ConsoleColor.Magenta, | |
| ResultKind.Video => ConsoleColor.Red, | |
| ResultKind.Audio => ConsoleColor.Blue, | |
| ResultKind.Archive => ConsoleColor.DarkYellow, | |
| ResultKind.Folder => ConsoleColor.Cyan, | |
| _ => ConsoleColor.Gray | |
| }; | |
| Console.ForegroundColor = color; | |
| Console.Write($" [{GetBadgeLabel(r.Kind),-4}] "); | |
| Console.ResetColor(); | |
| Console.WriteLine($"{r.Name} β {r.FullPath}"); | |
| } | |
| if (results.Count > 10) | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.WriteLine($" ... and {results.Count - 10} more"); | |
| Console.ResetColor(); | |
| } | |
| Console.WriteLine(); | |
| } | |
| } | |
| WaitExit(); | |
| } | |
| private static void WaitExit() | |
| { | |
| Console.WriteLine(); | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.WriteLine("Press Enter to exit..."); | |
| Console.ResetColor(); | |
| Console.ReadLine(); | |
| FreeConsole(); | |
| } | |
| private static void PrintHeader(string text) | |
| { | |
| Console.ForegroundColor = ConsoleColor.Cyan; | |
| Console.WriteLine($"βββ {text} βββ"); | |
| Console.ResetColor(); | |
| } | |
| private static void PrintDivider() | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.WriteLine(new string('β', 60)); | |
| Console.ResetColor(); | |
| } | |
| private static void PrintLine(ConsoleColor color, string label, string value) | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.Write($"{label,-10}: "); | |
| Console.ForegroundColor = color; | |
| Console.WriteLine(value); | |
| Console.ResetColor(); | |
| } | |
| private static void PrintDetail(string text) | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkGray; | |
| Console.WriteLine(text); | |
| Console.ResetColor(); | |
| } | |
| private static void PrintError(string text) | |
| { | |
| Console.ForegroundColor = ConsoleColor.Red; | |
| Console.WriteLine($"β {text}"); | |
| Console.ResetColor(); | |
| } | |
| private static void PrintInfo(string text) | |
| { | |
| Console.ForegroundColor = ConsoleColor.DarkCyan; | |
| Console.WriteLine($"βΉ {text}"); | |
| Console.ResetColor(); | |
| } | |
| private static string FormatBytes(long bytes) | |
| { | |
| if (bytes < 1024) return $"{bytes} B"; | |
| if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; | |
| if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB"; | |
| return $"{bytes / (1024.0 * 1024 * 1024):F2} GB"; | |
| } | |
| private static string GetBadgeLabel(ResultKind kind) => kind switch | |
| { | |
| ResultKind.App => "APP", | |
| ResultKind.Document => "DOC", | |
| ResultKind.Image => "IMG", | |
| ResultKind.Video => "VID", | |
| ResultKind.Audio => "AUD", | |
| ResultKind.Archive => "ZIP", | |
| ResultKind.Folder => "DIR", | |
| _ => "FILE" | |
| }; | |
| private void OnExit(object sender, ExitEventArgs e) | |
| { | |
| _shutdown = true; | |
| _hotkeyThread?.Join(500); | |
| if (_ownsMutex) | |
| _mutex?.ReleaseMutex(); | |
| _mutex?.Dispose(); | |
| Logger.Log("=== App exit ==="); | |
| } | |
| private void HotkeyLoop(object? param) | |
| { | |
| IntPtr targetHwnd = (IntPtr)(param ?? IntPtr.Zero); | |
| if (targetHwnd == IntPtr.Zero) return; | |
| uint modifiers = 0x0002 | 0x0004; | |
| if (!Win32Api.RegisterHotKey(IntPtr.Zero, 1, modifiers, 0x20)) | |
| { | |
| Logger.Log($"RegisterHotKey failed: {Marshal.GetLastWin32Error()}"); | |
| return; | |
| } | |
| try | |
| { | |
| while (!_shutdown) | |
| { | |
| int result = Win32Api.GetMessageW(out Win32Api.MSG msg, IntPtr.Zero, 0, 0); | |
| if (result == 0 || result == -1) break; | |
| if (msg.message == Win32Api.WM_HOTKEY) | |
| Win32Api.PostMessageW(targetHwnd, WM_TOGGLE_WINDOW, IntPtr.Zero, IntPtr.Zero); | |
| else | |
| { | |
| Win32Api.TranslateMessage(ref msg); | |
| Win32Api.DispatchMessageW(ref msg); | |
| } | |
| } | |
| } | |
| finally | |
| { | |
| Win32Api.UnregisterHotKey(IntPtr.Zero, 1); | |
| } | |
| } | |
| } | |