Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 61 additions & 44 deletions ContextMenuProfiler.Hook/src/injector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <tlhelp32.h>
#include <stdio.h>
#include <iostream>
#include <vector>

// Enable Debug Privilege (Required for injecting into system processes)
bool EnableDebugPrivilege()
Expand Down Expand Up @@ -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<DWORD> FindProcessIdsByName(const char* processName)
{
DWORD processId = 0;
std::vector<DWORD> 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);
Expand Down Expand Up @@ -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<DWORD> 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)
{
Expand Down Expand Up @@ -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<DWORD> 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)
Expand Down Expand Up @@ -278,4 +295,4 @@ int main(int argc, char* argv[])
}

return 0;
}
}
2 changes: 2 additions & 0 deletions ContextMenuProfiler.UI/Converters/LoadTimeToTextConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +37 to 39
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BenchmarkService can now set Status = "Skipped (Known Unstable)" with TotalTime = 0, but ShouldShowNa doesn’t treat "Skipped" as a non-measured outcome. This will display "0 ms" for skipped entries, reintroducing the misleading 0ms case the PR is trying to eliminate. Consider updating ShouldShowNa to return true for skipped entries (e.g., status.Contains("Skipped", ...)) and align the indentation of the added conditions with the surrounding lines.

Suggested change
status.Contains("Not Measured", StringComparison.OrdinalIgnoreCase) ||
status.Contains("Unsupported", StringComparison.OrdinalIgnoreCase) ||
status.Contains("No Menu", StringComparison.OrdinalIgnoreCase);
status.Contains("Not Measured", StringComparison.OrdinalIgnoreCase) ||
status.Contains("Unsupported", StringComparison.OrdinalIgnoreCase) ||
status.Contains("No Menu", StringComparison.OrdinalIgnoreCase) ||
status.Contains("Skipped", StringComparison.OrdinalIgnoreCase);

Copilot uses AI. Check for mistakes.
}

Expand Down
78 changes: 63 additions & 15 deletions ContextMenuProfiler.UI/Core/BenchmarkService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BenchmarkResult> RunSystemBenchmark(ScanMode mode = ScanMode.Targeted)
{
// Use Task.Run to avoid deadlocks on UI thread when waiting for async tasks
Expand Down Expand Up @@ -126,13 +134,14 @@ public async Task<List<BenchmarkResult>> 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"
};
Expand Down Expand Up @@ -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))
{
Expand All @@ -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")
Expand All @@ -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
{
Expand All @@ -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();
Expand Down
Loading
Loading