Files
whitemagic/WhiteMagic/Process/ApplicationFinder.cs
T
kbe 3e294dc846 Add process discovery helpers and Magic facade accessors
Introduces ApplicationFinder (by name/title/handle), Magic.Open overloads, and Magic.Threads/Regions/QueryRegion accessors. Closes section 3 of add-thread-region-finder and updates tasks/comparison doc.
2026-07-22 16:04:53 +02:00

143 lines
4.5 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)
{
throw new InvalidOperationException(
$"Process name '{processName}' is ambiguous ({candidates.Length} matches): " +
string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}")));
}
return candidates[0];
}
/// <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;
}
}