diff --git a/ContextMenuProfiler.Hook/src/injector.cpp b/ContextMenuProfiler.Hook/src/injector.cpp index f9f80ce..b0b5414 100644 --- a/ContextMenuProfiler.Hook/src/injector.cpp +++ b/ContextMenuProfiler.Hook/src/injector.cpp @@ -2,6 +2,7 @@ #include #include #include +#include // Enable Debug Privilege (Required for injecting into system processes) bool EnableDebugPrivilege() @@ -35,35 +36,34 @@ bool EnableDebugPrivilege() return true; } -// Helper to inject DLL into process by name -bool InjectDll(const char* processName, const char* dllPath) +static std::vector FindProcessIdsByName(const char* processName) { - DWORD processId = 0; + std::vector processIds; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (hSnapshot != INVALID_HANDLE_VALUE) + if (hSnapshot == INVALID_HANDLE_VALUE) { - PROCESSENTRY32 pe; - pe.dwSize = sizeof(PROCESSENTRY32); - if (Process32First(hSnapshot, &pe)) - { - do - { - if (_stricmp(pe.szExeFile, processName) == 0) - { - processId = pe.th32ProcessID; - break; - } - } while (Process32Next(hSnapshot, &pe)); - } - CloseHandle(hSnapshot); + return processIds; } - if (processId == 0) + PROCESSENTRY32 pe; + pe.dwSize = sizeof(PROCESSENTRY32); + if (Process32First(hSnapshot, &pe)) { - std::cerr << "Process not found: " << processName << std::endl; - return false; + do + { + if (_stricmp(pe.szExeFile, processName) == 0) + { + processIds.push_back(pe.th32ProcessID); + } + } while (Process32Next(hSnapshot, &pe)); } + CloseHandle(hSnapshot); + return processIds; +} + +static bool InjectDllToProcess(DWORD processId, const char* dllPath) +{ std::cout << "Target Process ID: " << processId << std::endl; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); @@ -130,35 +130,30 @@ bool InjectDll(const char* processName, const char* dllPath) return exitCode != 0; } -// Helper to eject DLL from process by name -bool EjectDll(const char* processName, const char* dllName) +// Helper to inject DLL into all matching processes by name +bool InjectDll(const char* processName, const char* dllPath) { - DWORD processId = 0; - HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (hSnapshot != INVALID_HANDLE_VALUE) + std::vector processIds = FindProcessIdsByName(processName); + if (processIds.empty()) { - PROCESSENTRY32 pe; - pe.dwSize = sizeof(PROCESSENTRY32); - if (Process32First(hSnapshot, &pe)) - { - do - { - if (_stricmp(pe.szExeFile, processName) == 0) - { - processId = pe.th32ProcessID; - break; - } - } while (Process32Next(hSnapshot, &pe)); - } - CloseHandle(hSnapshot); + std::cerr << "Process not found: " << processName << std::endl; + return false; } - if (processId == 0) + bool anySuccess = false; + for (DWORD pid : processIds) { - std::cerr << "Process not found: " << processName << std::endl; - return false; + if (InjectDllToProcess(pid, dllPath)) + { + anySuccess = true; + } } + return anySuccess; +} + +static bool EjectDllFromProcess(DWORD processId, const char* dllName) +{ HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); if (!hProcess) { @@ -211,6 +206,28 @@ bool EjectDll(const char* processName, const char* dllName) return true; } +// Helper to eject DLL from all matching processes by name +bool EjectDll(const char* processName, const char* dllName) +{ + std::vector processIds = FindProcessIdsByName(processName); + if (processIds.empty()) + { + std::cerr << "Process not found: " << processName << std::endl; + return false; + } + + bool anySuccess = false; + for (DWORD pid : processIds) + { + if (EjectDllFromProcess(pid, dllName)) + { + anySuccess = true; + } + } + + return anySuccess; +} + int main(int argc, char* argv[]) { if (argc < 2) @@ -278,4 +295,4 @@ int main(int argc, char* argv[]) } return 0; -} \ No newline at end of file +} diff --git a/ContextMenuProfiler.UI/Converters/LoadTimeToTextConverter.cs b/ContextMenuProfiler.UI/Converters/LoadTimeToTextConverter.cs index b405fab..d533dd9 100644 --- a/ContextMenuProfiler.UI/Converters/LoadTimeToTextConverter.cs +++ b/ContextMenuProfiler.UI/Converters/LoadTimeToTextConverter.cs @@ -34,6 +34,8 @@ private static bool ShouldShowNa(string status, long ms) status.Contains("Load Error", StringComparison.OrdinalIgnoreCase) || status.Contains("Orphaned", StringComparison.OrdinalIgnoreCase) || status.Contains("Missing", StringComparison.OrdinalIgnoreCase) || + status.Contains("Not Measured", StringComparison.OrdinalIgnoreCase) || + status.Contains("Unsupported", StringComparison.OrdinalIgnoreCase) || status.Contains("No Menu", StringComparison.OrdinalIgnoreCase); } diff --git a/ContextMenuProfiler.UI/Core/BenchmarkService.cs b/ContextMenuProfiler.UI/Core/BenchmarkService.cs index ff0bf87..a0f5c0a 100644 --- a/ContextMenuProfiler.UI/Core/BenchmarkService.cs +++ b/ContextMenuProfiler.UI/Core/BenchmarkService.cs @@ -56,6 +56,14 @@ internal class ClsidMetadata public class BenchmarkService { + private static readonly string[] KnownUnstableHandlerTokens = + { + "PintoStartScreen", + "NvcplDesktopContext", + "NvAppDesktopContext", + "NVIDIA CPL Context Menu Extension" + }; + public List RunSystemBenchmark(ScanMode mode = ScanMode.Targeted) { // Use Task.Run to avoid deadlocks on UI thread when waiting for async tasks @@ -126,13 +134,14 @@ public async Task> RunSystemBenchmarkAsync(ScanMode mode = { Name = name, Type = "Static", - Status = "OK", + Status = "Static (Not Measured)", BinaryPath = ExtractExecutablePath(command), RegistryEntries = paths.Select(p => new RegistryHandlerInfo { Path = p, Location = $"Registry (Shell) - {p.Split('\\')[0]}" }).ToList(), InterfaceType = "Static Verb", + DetailedStatus = "Static shell verbs do not go through Hook COM probing and are displayed as not measured.", TotalTime = 0, Category = "Static" }; @@ -176,6 +185,18 @@ private async Task EnrichBenchmarkResultAsync(BenchmarkResult result, string con { if (!result.Clsid.HasValue) return; + if (IsKnownUnstableHandler(result)) + { + result.Status = "Skipped (Known Unstable)"; + result.DetailedStatus = "Skipped Hook invocation for a known unstable system handler to avoid scan-wide IPC stalls."; + result.InterfaceType = "Skipped"; + result.CreateTime = 0; + result.InitTime = 0; + result.QueryTime = 0; + result.TotalTime = 0; + return; + } + // Check for Orphaned / Missing DLL if (!string.IsNullOrEmpty(result.BinaryPath) && !File.Exists(result.BinaryPath)) { @@ -195,7 +216,11 @@ private async Task EnrichBenchmarkResultAsync(BenchmarkResult result, string con result.InterfaceType = hookData.@interface; if (!string.IsNullOrEmpty(hookData.names)) { - result.Name = hookData.names.Replace("|", ", "); + // Keep packaged/UWP display names stable to avoid garbled menu-title replacements. + if (!string.Equals(result.Type, "UWP", StringComparison.OrdinalIgnoreCase)) + { + result.Name = hookData.names.Replace("|", ", "); + } if (result.Status == "Unknown") result.Status = "Verified via Hook"; } else if (result.Status == "Unknown" || result.Status == "OK") @@ -219,25 +244,17 @@ private async Task EnrichBenchmarkResultAsync(BenchmarkResult result, string con } else if (hookData != null && !hookData.success) { - if (!string.IsNullOrEmpty(hookData.error) && hookData.error.Contains("Timeout", StringComparison.OrdinalIgnoreCase)) - { - result.Status = "IPC Timeout"; - result.DetailedStatus = $"Hook service timed out while probing this extension. Error: {hookData.error}"; - } - else - { - result.Status = "Load Error"; - result.DetailedStatus = $"The Hook service failed to load this extension. Error: {hookData.error ?? "Unknown Error"}"; - } + result.Status = "Load Error"; + result.DetailedStatus = $"The Hook service failed to load this extension. Error: {hookData.error ?? "Unknown Error"}"; } else if (hookData == null) { if (result.Status != "Load Error" && result.Status != "Orphaned / Missing DLL") { - if (hookCall.roundtrip_ms >= 1900) + if (string.Equals(result.Type, "UWP", StringComparison.OrdinalIgnoreCase)) { - result.Status = "IPC Timeout"; - result.DetailedStatus = "Hook service response timed out for this extension. Data is based on registry scan only."; + result.Status = "Unsupported (UWP)"; + result.DetailedStatus = "This UWP/packaged extension could not be benchmarked via current Hook path on this system."; } else { @@ -248,6 +265,37 @@ private async Task EnrichBenchmarkResultAsync(BenchmarkResult result, string con } } + private static bool IsKnownUnstableHandler(BenchmarkResult result) + { + if (!string.IsNullOrWhiteSpace(result.Name) && + KnownUnstableHandlerTokens.Any(token => result.Name.Contains(token, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + if (!string.IsNullOrWhiteSpace(result.FriendlyName) && + KnownUnstableHandlerTokens.Any(token => result.FriendlyName.Contains(token, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + if (result.RegistryEntries != null) + { + foreach (var entry in result.RegistryEntries) + { + if ((!string.IsNullOrWhiteSpace(entry.Location) && + KnownUnstableHandlerTokens.Any(token => entry.Location.Contains(token, StringComparison.OrdinalIgnoreCase))) || + (!string.IsNullOrWhiteSpace(entry.Path) && + KnownUnstableHandlerTokens.Any(token => entry.Path.Contains(token, StringComparison.OrdinalIgnoreCase)))) + { + return true; + } + } + } + + return false; + } + private ClsidMetadata QueryClsidMetadata(Guid clsid, int depth = 0) { var meta = new ClsidMetadata(); diff --git a/ContextMenuProfiler.UI/Core/HookIpcClient.cs b/ContextMenuProfiler.UI/Core/HookIpcClient.cs index a81e985..199e71c 100644 --- a/ContextMenuProfiler.UI/Core/HookIpcClient.cs +++ b/ContextMenuProfiler.UI/Core/HookIpcClient.cs @@ -37,113 +37,161 @@ public class HookCallResult public static class HookIpcClient { private const string PipeName = "ContextMenuProfilerHook"; - private const int ConnectTimeoutMs = 1200; - private const int RoundTripTimeoutMs = 2000; - private static readonly SemaphoreSlim _lock = new SemaphoreSlim(3, 3); + private const int LockAcquireTimeoutMs = 5000; + private const int ConnectTimeoutMs = 500; + private const int RoundTripTimeoutMs = 3500; + internal static readonly SemaphoreSlim IpcLock = new SemaphoreSlim(1, 1); public static async Task GetHookDataAsync(string clsid, string? contextPath = null, string? dllHint = null) { var result = new HookCallResult(); var swTotal = Stopwatch.StartNew(); - - // Default bait path if none provided - string path = contextPath ?? Path.Combine(Path.GetTempPath(), "ContextMenuProfiler_probe.txt"); - if (!File.Exists(path) && !Directory.Exists(path)) - { - try { File.WriteAllText(path, "probe"); } catch {} - } - - var swLock = Stopwatch.StartNew(); - await _lock.WaitAsync(); - swLock.Stop(); - result.lock_wait_ms = Math.Max(0, (long)swLock.Elapsed.TotalMilliseconds); try { - using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.Asynchronous)) + // Default bait path if none provided + string path = contextPath ?? Path.Combine(Path.GetTempPath(), "ContextMenuProfiler_probe.txt"); + if (!File.Exists(path) && !Directory.Exists(path)) { - var swConnect = Stopwatch.StartNew(); - bool connected = false; - for (int attempt = 0; attempt < 2 && !connected; attempt++) + try { File.WriteAllText(path, "probe"); } catch {} + } + + for (int attempt = 0; attempt < 2; attempt++) + { + bool hasLock = false; + try { - try + var swLock = Stopwatch.StartNew(); + hasLock = await IpcLock.WaitAsync(LockAcquireTimeoutMs); + swLock.Stop(); + result.lock_wait_ms += Math.Max(0, (long)swLock.Elapsed.TotalMilliseconds); + if (!hasLock) { - await client.ConnectAsync(ConnectTimeoutMs); - connected = true; + if (attempt == 0) + { + await Task.Delay(150); + continue; + } + return result; } - catch when (attempt == 0) + + using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.Asynchronous)) { - await Task.Delay(80); - } - } - if (!connected) - { - swConnect.Stop(); - result.connect_ms = Math.Max(0, (long)swConnect.Elapsed.TotalMilliseconds); - Debug.WriteLine("[IPC DIAG] Connection Failed after retry."); - return result; - } - swConnect.Stop(); - result.connect_ms = Math.Max(0, (long)swConnect.Elapsed.TotalMilliseconds); + var swConnect = Stopwatch.StartNew(); + try + { + await client.ConnectAsync(ConnectTimeoutMs); + } + catch (Exception ex) + { + swConnect.Stop(); + result.connect_ms += Math.Max(0, (long)swConnect.Elapsed.TotalMilliseconds); + Debug.WriteLine($"[IPC DIAG] Connection Failed: {ex.Message}"); + if (attempt == 0) + { + await Task.Delay(120); + continue; + } + return result; + } + swConnect.Stop(); + result.connect_ms += Math.Max(0, (long)swConnect.Elapsed.TotalMilliseconds); - var swRoundTrip = Stopwatch.StartNew(); - using var roundTripCts = new CancellationTokenSource(RoundTripTimeoutMs); - // Format: "CLSID|Path[|DllHint]" - string requestStr = string.IsNullOrEmpty(dllHint) ? $"{clsid}|{path}" : $"{clsid}|{path}|{dllHint}"; - byte[] request = Encoding.UTF8.GetBytes(requestStr); - await client.WriteAsync(request, 0, request.Length, roundTripCts.Token); - await client.FlushAsync(roundTripCts.Token); + var swRoundTrip = Stopwatch.StartNew(); + // Format: "CLSID|Path[|DllHint]" + string requestStr = string.IsNullOrEmpty(dllHint) ? $"{clsid}|{path}" : $"{clsid}|{path}|{dllHint}"; + byte[] request = Encoding.UTF8.GetBytes(requestStr); + await client.WriteAsync(request, 0, request.Length); - byte[] responseBuf = new byte[65536]; - int read = await client.ReadAsync(responseBuf, 0, responseBuf.Length, roundTripCts.Token); - - if (read <= 0) - { - swRoundTrip.Stop(); - result.roundtrip_ms = Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); - return result; - } + byte[] responseBuf = new byte[65536]; + int read; + try + { + read = await client.ReadAsync(responseBuf, 0, responseBuf.Length).WaitAsync(TimeSpan.FromMilliseconds(RoundTripTimeoutMs)); + } + catch (TimeoutException) + { + swRoundTrip.Stop(); + result.roundtrip_ms += Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); + if (attempt == 0) + { + await Task.Delay(120); + continue; + } + return result; + } + + if (read <= 0) + { + swRoundTrip.Stop(); + result.roundtrip_ms += Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); + if (attempt == 0) + { + await Task.Delay(120); + continue; + } + return result; + } - string response = Encoding.UTF8.GetString(responseBuf, 0, read).TrimEnd('\0'); - try + string response = Encoding.UTF8.GetString(responseBuf, 0, read).TrimEnd('\0'); + try + { + // 寻找第一个 { 和最后一个 } 确保 JSON 完整 + int start = response.IndexOf('{'); + int end = response.LastIndexOf('}'); + if (start != -1 && end != -1 && end > start) + { + string json = response.Substring(start, end - start + 1); + result.data = JsonSerializer.Deserialize(json); + swRoundTrip.Stop(); + result.roundtrip_ms += Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); + return result; + } + swRoundTrip.Stop(); + result.roundtrip_ms += Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); + if (attempt == 0) + { + await Task.Delay(120); + continue; + } + return result; + } + catch (JsonException) + { + swRoundTrip.Stop(); + result.roundtrip_ms += Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); + if (attempt == 0) + { + await Task.Delay(120); + continue; + } + return result; + } + } + } + catch (Exception ex) { - // 寻找第一个 { 和最后一个 } 确保 JSON 完整 - int start = response.IndexOf('{'); - int end = response.LastIndexOf('}'); - if (start != -1 && end != -1 && end > start) + Debug.WriteLine($"[IPC DIAG] Critical Error: {ex.Message}"); + if (attempt == 0) { - string json = response.Substring(start, end - start + 1); - result.data = JsonSerializer.Deserialize(json); - swRoundTrip.Stop(); - result.roundtrip_ms = Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); - return result; + await Task.Delay(120); + continue; } - swRoundTrip.Stop(); - result.roundtrip_ms = Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); return result; } - catch (JsonException) + finally { - swRoundTrip.Stop(); - result.roundtrip_ms = Math.Max(0, (long)swRoundTrip.Elapsed.TotalMilliseconds); - return result; + if (hasLock) + { + IpcLock.Release(); + } } } - } - catch (OperationCanceledException) - { - Debug.WriteLine("[IPC DIAG] Roundtrip timeout."); - return result; - } - catch (Exception ex) - { - Debug.WriteLine($"[IPC DIAG] Critical Error: {ex.Message}"); return result; } finally { swTotal.Stop(); result.total_ms = Math.Max(0, (long)swTotal.Elapsed.TotalMilliseconds); - _lock.Release(); } } diff --git a/ContextMenuProfiler.UI/Core/RegistryScanner.cs b/ContextMenuProfiler.UI/Core/RegistryScanner.cs index 78f089f..9de0049 100644 --- a/ContextMenuProfiler.UI/Core/RegistryScanner.cs +++ b/ContextMenuProfiler.UI/Core/RegistryScanner.cs @@ -54,8 +54,13 @@ public static Dictionary> ScanHandlers(ScanMode { string[] rootKeys = Registry.ClassesRoot.GetSubKeyNames(); - // Use Parallel.ForEach for faster scanning - Parallel.ForEach(rootKeys, keyName => + // Bound parallelism to avoid saturating CPU during deep scans. + var options = new ParallelOptions + { + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) + }; + + Parallel.ForEach(rootKeys, options, keyName => { if (keyName.StartsWith(".")) { @@ -246,4 +251,4 @@ private static void ScanShellKey(ConcurrentDictionary> verb } } } -} \ No newline at end of file +} diff --git a/ContextMenuProfiler.UI/Core/Services/HookService.cs b/ContextMenuProfiler.UI/Core/Services/HookService.cs index 61e7fb2..185e50c 100644 --- a/ContextMenuProfiler.UI/Core/Services/HookService.cs +++ b/ContextMenuProfiler.UI/Core/Services/HookService.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Pipes; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ContextMenuProfiler.UI.Core.Services; @@ -35,6 +36,11 @@ public HookStatus CurrentStatus private const string PipeName = "ContextMenuProfilerHook"; private const string DllName = "ContextMenuProfiler.Hook.dll"; private const string InjectorName = "ContextMenuProfiler.Injector.exe"; + private const int StatusFailureThreshold = 3; + private static readonly TimeSpan ActiveGraceWindow = TimeSpan.FromSeconds(20); + private static readonly TimeSpan DisconnectGraceWindow = TimeSpan.FromMinutes(3); + private int _consecutiveStatusFailures; + private DateTime _lastKnownActiveAtUtc = DateTime.MinValue; private HookService() { @@ -80,53 +86,107 @@ public async Task GetStatusAsync() bool isPipeActive = await CheckPipeAsync(); if (isPipeActive) { + _consecutiveStatusFailures = 0; + _lastKnownActiveAtUtc = DateTime.UtcNow; CurrentStatus = HookStatus.Active; return HookStatus.Active; } + _consecutiveStatusFailures++; + + if (CurrentStatus == HookStatus.Active && + _consecutiveStatusFailures < StatusFailureThreshold && + (DateTime.UtcNow - _lastKnownActiveAtUtc) <= ActiveGraceWindow) + { + return HookStatus.Active; + } + // 2. If pipe failed, check if DLL is actually injected. // This is the "heavy" check, so we do it only if pipe fails. bool isInjected = await Task.Run(() => IsDllInjected()); var status = isInjected ? HookStatus.Injected : HookStatus.Disconnected; + + // Some systems intermittently fail module enumeration or pipe probing after heavy scans. + // If we were recently active, prefer "Injected" instead of flapping to "Disconnected". + if (!isInjected && + CurrentStatus != HookStatus.Disconnected && + _lastKnownActiveAtUtc != DateTime.MinValue && + (DateTime.UtcNow - _lastKnownActiveAtUtc) <= DisconnectGraceWindow) + { + status = HookStatus.Injected; + } + + if (status != HookStatus.Disconnected) + { + _consecutiveStatusFailures = 0; + } + else if (CurrentStatus != HookStatus.Disconnected && _consecutiveStatusFailures < StatusFailureThreshold) + { + return CurrentStatus; + } + CurrentStatus = status; return status; } - private Process? _cachedExplorer; - private bool IsDllInjected() { try { - // Cache explorer process to avoid repeated lookups - if (_cachedExplorer == null || _cachedExplorer.HasExited) + // Multiple explorer.exe instances may exist. + // Treat injected as true if any explorer process has the module loaded. + foreach (var proc in Process.GetProcessesByName("explorer")) { - _cachedExplorer = Process.GetProcessesByName("explorer").FirstOrDefault(); + try + { + if (proc.HasExited) continue; + if (proc.Modules.Cast().Any(m => m.ModuleName.Equals(DllName, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + } + catch + { + // Ignore per-process access/read failures and continue checking others. + } + finally + { + proc.Dispose(); + } } - - if (_cachedExplorer == null) return false; - - // Process.Modules is expensive. We only call this when pipe check fails. - _cachedExplorer.Refresh(); // Ensure we have latest module list - return _cachedExplorer.Modules.Cast().Any(m => m.ModuleName.Equals(DllName, StringComparison.OrdinalIgnoreCase)); + return false; } catch { - _cachedExplorer = null; return false; } } private async Task CheckPipeAsync() { + bool hasLock = false; try { + // Reuse shared IPC lock to avoid status probe interfering with active benchmark requests. + hasLock = await HookIpcClient.IpcLock.WaitAsync(150); + if (!hasLock) + { + return CurrentStatus == HookStatus.Active; + } + using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.Asynchronous); - await client.ConnectAsync(700); + await client.ConnectAsync(1200); return client.IsConnected; } catch { return false; } + finally + { + if (hasLock) + { + HookIpcClient.IpcLock.Release(); + } + } } public async Task InjectAsync() @@ -143,7 +203,13 @@ public async Task InjectAsync() return false; } - return await RunCommandAsync(injectorPath, $"\"{dllPath}\""); + bool ok = await RunCommandAsync(injectorPath, $"\"{dllPath}\""); + if (ok) + { + _lastKnownActiveAtUtc = DateTime.UtcNow; + _consecutiveStatusFailures = 0; + } + return ok; } public async Task EjectAsync() @@ -154,7 +220,14 @@ public async Task EjectAsync() if (string.IsNullOrEmpty(injectorPath)) return false; // For ejection, we just need the filename, but the injector handles path to name conversion - return await RunCommandAsync(injectorPath, $"\"{dllPath}\" --eject"); + bool ok = await RunCommandAsync(injectorPath, $"\"{dllPath}\" --eject"); + if (ok) + { + _lastKnownActiveAtUtc = DateTime.MinValue; + _consecutiveStatusFailures = 0; + CurrentStatus = HookStatus.Disconnected; + } + return ok; } private string FindFile(string fileName) diff --git a/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs b/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs index 88efb61..caf51d3 100644 --- a/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs +++ b/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs @@ -174,6 +174,8 @@ private static Dictionary BuildEnglish() ["Dashboard.Sort.LoadAsc"] = "Load Time (Low to High)", ["Dashboard.Sort.NameAsc"] = "Name (A-Z)", ["Dashboard.Sort.Latest"] = "Latest Scanned First", + ["Dashboard.FilterMeasuredOnly"] = "Measured only", + ["Dashboard.FilterMeasuredOnlyTip"] = "Show only entries with valid measured load time (> 0 ms)", ["Dashboard.Status.ScanningSystem"] = "Scanning system...", ["Dashboard.Status.ScanningFile"] = "Scanning: {0}", ["Dashboard.Status.ScanComplete"] = "Scan complete. Found {0} extensions.", @@ -264,6 +266,8 @@ private static Dictionary BuildChinese() ["Dashboard.Sort.LoadAsc"] = "加载时间(低到高)", ["Dashboard.Sort.NameAsc"] = "名称(A-Z)", ["Dashboard.Sort.Latest"] = "最近扫描优先", + ["Dashboard.FilterMeasuredOnly"] = "仅看已测量", + ["Dashboard.FilterMeasuredOnlyTip"] = "仅显示有有效测量耗时(> 0 ms)的项目", ["Dashboard.Status.ScanningSystem"] = "正在扫描系统...", ["Dashboard.Status.ScanningFile"] = "正在扫描:{0}", ["Dashboard.Status.ScanComplete"] = "扫描完成,共找到 {0} 个扩展。", diff --git a/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs b/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs index fb3c43f..578c914 100644 --- a/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs +++ b/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs @@ -125,6 +125,11 @@ await App.Current.Dispatcher.InvokeAsync(() => private bool MatchesFilter(BenchmarkResult result) { + if (ShowMeasuredOnly && result.TotalTime <= 0) + { + return false; + } + // Category Match bool categoryMatch = SelectedCategory == "All" || result.Category == SelectedCategory; if (!categoryMatch) return false; @@ -178,6 +183,14 @@ partial void OnIsBusyChanged(bool value) [ObservableProperty] private bool _useDeepScan = false; + [ObservableProperty] + private bool _showMeasuredOnly = false; + + partial void OnShowMeasuredOnlyChanged(bool value) + { + _ = ApplyFilterAsync(); + } + [ObservableProperty] private string _realLoadTime = "N/A"; // Display string for Real Shell Benchmark diff --git a/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml b/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml index 3687df3..87a41db 100644 --- a/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml +++ b/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml @@ -196,12 +196,19 @@ - - - - - - + + + + + + + + + +