- Drop the false WOW64 claim from GetContext32/SetContext32 docs and guard them for 32-bit targets only.\n- Make FrozenThread dispose the thread handles it owns; make Freeze(predicate) dispose filtered-out threads.\n- Pass the already-validated handle through GetThreadById instead of opening a second one.\n- Add no-progress guard to MemoryBase.EnumerateRegions.\n- Dispose unmatched Process candidates in ApplicationFinder.OpenProcess.\n- Clean up RemoteThreadExecutor allocation formatting.
150 lines
4.7 KiB
C#
150 lines
4.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using WhiteMagic.Native;
|
|
using WhiteMagic.Windows;
|
|
|
|
namespace WhiteMagic.ProcessDiscovery;
|
|
|
|
/// <summary>
|
|
/// Discovers running processes by name, window title, or window handle so they can be
|
|
/// attached through a <see cref="Magic"/> session.
|
|
/// </summary>
|
|
public static class ApplicationFinder
|
|
{
|
|
/// <summary>
|
|
/// Enumerates processes whose image name matches <paramref name="processName"/>
|
|
/// (extension optional).
|
|
/// </summary>
|
|
public static IEnumerable<Process> Enumerate(string processName)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(processName);
|
|
|
|
return Process.GetProcessesByName(GetNameWithoutExtension(processName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the unique process whose image name matches <paramref name="processName"/>.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">Zero or multiple processes match.</exception>
|
|
public static Process OpenProcess(string processName)
|
|
{
|
|
Process[] candidates = Enumerate(processName).ToArray();
|
|
|
|
if (candidates.Length == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"No process named '{processName}' was found.");
|
|
}
|
|
|
|
if (candidates.Length > 1)
|
|
{
|
|
string list = string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}"));
|
|
foreach (Process candidate in candidates)
|
|
candidate.Dispose();
|
|
|
|
throw new InvalidOperationException(
|
|
$"Process name '{processName}' is ambiguous ({candidates.Length} matches): {list}");
|
|
}
|
|
|
|
Process result = candidates[0];
|
|
for (int i = 1; i < candidates.Length; i++)
|
|
candidates[i].Dispose();
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates processes that own a top-level window whose title equals
|
|
/// <paramref name="title"/>.
|
|
/// </summary>
|
|
public static IEnumerable<Process> FindByWindowTitle(string title)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(title);
|
|
|
|
var seen = new HashSet<int>();
|
|
foreach (RemoteWindow window in WindowFactory.GetWindows())
|
|
{
|
|
if (!string.Equals(window.Text, title, StringComparison.Ordinal))
|
|
continue;
|
|
|
|
uint pid = window.ProcessId;
|
|
if (pid == 0 || !seen.Add((int)pid))
|
|
continue;
|
|
|
|
Process? process;
|
|
try
|
|
{
|
|
process = global::System.Diagnostics.Process.GetProcessById((int)pid);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
yield return process;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the unique process that owns a top-level window titled <paramref name="title"/>.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">Zero or multiple windows match.</exception>
|
|
public static Process OpenByWindowTitle(string title)
|
|
{
|
|
Process[] candidates = FindByWindowTitle(title).ToArray();
|
|
|
|
if (candidates.Length == 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"No top-level window titled '{title}' was found.");
|
|
}
|
|
|
|
if (candidates.Length > 1)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Window title '{title}' is ambiguous ({candidates.Length} matches): " +
|
|
string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}")));
|
|
}
|
|
|
|
return candidates[0];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the process that owns the specified window handle.
|
|
/// </summary>
|
|
public static Process OpenByWindowHandle(IntPtr handle)
|
|
{
|
|
if (handle == IntPtr.Zero)
|
|
throw new ArgumentException("Window handle cannot be zero.", nameof(handle));
|
|
|
|
uint tid = NativeMethods.GetWindowThreadProcessId(handle, out uint processId);
|
|
if (tid == 0 || processId == 0)
|
|
{
|
|
int error = Marshal.GetLastPInvokeError();
|
|
throw new InvalidOperationException(
|
|
$"GetWindowThreadProcessId failed for handle {handle:X}: error {error}.");
|
|
}
|
|
|
|
try
|
|
{
|
|
return global::System.Diagnostics.Process.GetProcessById((int)processId);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Process {processId} owning window {handle:X} is no longer running.");
|
|
}
|
|
}
|
|
|
|
private static string GetNameWithoutExtension(string name)
|
|
{
|
|
if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
|
return name[..^4];
|
|
|
|
return name;
|
|
}
|
|
}
|